Skip to content

Commit 4ee3f7f

Browse files
committed
Support hexadecimal-encoded hashes.
Support hexadecimal as an alternative to ZBase32 for encoding the hash of a mapping record. When using SSSOM-CLI's --extract option, the hexadecimal hash can be obtained by `mapping(N).special(hexhash)` (instead of `mapping(N).special(hash)` for the normal ZBase32 hash). There is an ongoing discussion on the SSSOM spec about possibly using hexadecimal instead of ZBase32 for the standard hash.
1 parent 419785e commit 4ee3f7f

5 files changed

Lines changed: 112 additions & 4 deletions

File tree

core/src/main/java/org/incenp/obofoundry/sssom/MappingHasher.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,29 @@ public class MappingHasher {
3333
private static char[] ZB32 = { 'y', 'b', 'n', 'd', 'r', 'f', 'g', '8', 'e', 'j', 'k', 'm', 'c', 'p', 'q', 'x', 'o',
3434
't', '1', 'u', 'w', 'i', 's', 'z', 'a', '3', '4', '5', 'h', '7', '6', '9' };
3535
private MessageDigest md;
36+
private boolean useHex;
3637

3738
/**
3839
* Creates a new instance.
3940
*/
4041
public MappingHasher() {
42+
this(false);
43+
}
44+
45+
/**
46+
* Creates a new instances that produces hexadecimal-encoded hash values.
47+
*
48+
* @param useHex If <code>true</code>, this instance will produce hash values
49+
* that are encoded in hexadecimal, rather than in ZBase32
50+
* encoding.
51+
*/
52+
public MappingHasher(boolean useHex) {
4153
try {
4254
md = MessageDigest.getInstance("SHA-256");
4355
} catch ( NoSuchAlgorithmException e ) {
56+
4457
}
58+
this.useHex = useHex;
4559
}
4660

4761
/**
@@ -51,15 +65,40 @@ public MappingHasher() {
5165
* @return The unique hash for the mapping.
5266
*/
5367
public String hash(Mapping mapping) {
68+
byte[] digest;
5469
if ( md != null ) {
55-
byte[] digest = md.digest(mapping.toSExpr().getBytes(StandardCharsets.UTF_8));
70+
digest = md.digest(mapping.toSExpr().getBytes(StandardCharsets.UTF_8));
5671
md.reset();
57-
return toZBase32(digest);
5872
} else {
5973
// SHA2-256 not available? This should probably never happen, but just in case
6074
// we fall back to the built-in Java hash code.
61-
return Integer.toHexString(mapping.hashCode());
75+
digest = new byte[4];
76+
int hashcode = mapping.hashCode();
77+
digest[0] = (byte) (hashcode & 0xFF);
78+
digest[1] = (byte) ((hashcode & 0xFF00) >> 8);
79+
digest[2] = (byte) ((hashcode & 0xFF0000) >> 16);
80+
digest[3] = (byte) ((hashcode & 0xFF000000) >> 24);
6281
}
82+
83+
return useHex ? toHexadecimal(digest) : toZBase32(digest);
84+
}
85+
86+
/**
87+
* Encodes a buffer into its hexadecimal representation.
88+
*
89+
* @param digest The input buffer to encode.
90+
* @return The hexadecimal representation of the input buffer.
91+
*/
92+
public static String toHexadecimal(byte[] digest) {
93+
StringBuffer sb = new StringBuffer();
94+
for ( int i = 0; i < digest.length; i++ ) {
95+
int high = (digest[i] & 0xF0) >> 4;
96+
int low = digest[i] & 0x0F;
97+
98+
sb.append((char) (high >= 10 ? high - 10 + 'A' : high + '0'));
99+
sb.append((char) (low >= 10 ? low - 10 + 'A' : low + '0'));
100+
}
101+
return sb.toString();
63102
}
64103

65104
/**

core/src/main/java/org/incenp/obofoundry/sssom/extract/HashExtractor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
/**
2525
* Extracts the standard SSSOM hash for a mapping.
2626
* <p>
27-
* This is used by expressions of the form {@code mapping(N).special.hash}.
27+
* This is used by expressions of the form {@code mapping(N).special(hash)}.
2828
*/
2929
public class HashExtractor extends MappingValueExtractor {
3030

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* SSSOM-Java - SSSOM library for Java
3+
* Copyright © 2026 Damien Goutte-Gattat
4+
*
5+
* This program is free software; you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.incenp.obofoundry.sssom.extract;
20+
21+
import org.incenp.obofoundry.sssom.MappingHasher;
22+
import org.incenp.obofoundry.sssom.model.Mapping;
23+
24+
/**
25+
* Extracts a hexadecimal-encoded form of the standard SSSOM hash for a mapping.
26+
* <p>
27+
* This is used by expressions of the form {@code mapping(N).special(hexhash)}.
28+
*/
29+
public class HexHashExtractor extends MappingValueExtractor {
30+
31+
private MappingHasher hasher = new MappingHasher(true);
32+
33+
/**
34+
* Creates a new instance.
35+
*
36+
* @param mappingNo The 0-based index of the mapping from which to extract the
37+
* hash, or (if negative) the 1-based index starting from the
38+
* last mapping.
39+
*/
40+
public HexHashExtractor(int mappingNo) {
41+
super(mappingNo);
42+
}
43+
44+
@Override
45+
public Class<?> getType() {
46+
return String.class;
47+
}
48+
49+
@Override
50+
protected Object extract(Mapping mapping) {
51+
return hasher.hash(mapping);
52+
}
53+
}

core/src/main/java/org/incenp/obofoundry/sssom/extract/ValueExtractorFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ private IValueExtractor parseMappingExtractor(int mappingNo, String expression)
106106
return new SExpressionExtractor(mappingNo);
107107
} else if ( expression.equals("special(hash)") ) {
108108
return new HashExtractor(mappingNo);
109+
} else if ( expression.equals("special(hexhash") ) {
110+
return new HexHashExtractor(mappingNo);
109111
}
110112

111113
throw new ExtractorSyntaxException();

core/src/test/java/org/incenp/obofoundry/sssom/MappingHasherTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,18 @@ void testSimpleHashing() {
4949
String hash = "4jfngj8y8bh9fu7ahhj9ic6miqz78cskxhyo61zkgb3gjte3ocuo";
5050
Assertions.assertEquals(hash, new MappingHasher().hash(m));
5151
}
52+
53+
@Test
54+
void testHexadecimalEncoding() {
55+
Mapping m = new Mapping();
56+
m.setSubjectId("SUBJECT");
57+
m.getAuthorId(true).add("AUTHOR");
58+
m.setConfidence(0.7);
59+
m.getExtensions(true).put("PROPERTY", new ExtensionValue(LocalDate.of(2025, 6, 1)));
60+
61+
// Expected hex-encoded SHA2-256 hash of
62+
// "(7:mapping((10:subject_id7:SUBJECT)(9:author_id(6:AUTHOR))(10:confidence3:0.7)(10:extensions((8:PROPERTY10:2025-06-01)))))"
63+
String hash = "D24A2324E03879F2CFB8E713FAB3CBABAFD3B2CA7F010F4AEA307264C5198327";
64+
Assertions.assertEquals(hash, new MappingHasher(true).hash(m));
65+
}
5266
}

0 commit comments

Comments
 (0)