Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 3 additions & 49 deletions dag/extdeps/git/object_store.dag
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import extdeps.git { GitAuthor, ObjectType, BlobObj, TreeObj, CommitObj, TagObj
import extdeps.object_storage { ObjectContainer }
import extdeps.object_storage.content_addressed { CasAddressingConfig }
import extdeps.external_authority { ExternalAuthority }
import extdeps.numeric.base16 { base16_decode_lower }
import extdeps.uri { Uri, Https }

data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
Expand Down Expand Up @@ -325,7 +326,7 @@ type GitObjectId

data git_object_id_text_note: String = "THE VALIDATING CONSTRUCTORS. A bare string cast into Sha1Digest or Sha256Digest admits any string: before these existed, a consumer casting text into one could produce a GitSha1ObjectId carrying \"x\", a 64-digit value labelled sha1, or arbitrary non-hex, and every one of those typechecked as a Git object id (review 2026-07-30, on the merge-admission wire parser that did exactly this). Family is not enough — LENGTH and SYNTAX are what make an object id an object id, and they differ per family, which is precisely why the discriminant exists.

These reuse git_decode_lower_hex_octets rather than introducing a second hex reader, so the canonical form is one authority: git spells object ids in LOWERCASE hex, and an uppercase or mixed-case spelling refuses rather than being folded, because folding would make two textual forms name one object and quietly break the round trip. A caller holding a foreign-cased id normalizes deliberately at its own boundary."
These reuse extdeps.numeric.base16 base16_decode_lower rather than introducing a second hex reader, so the canonical form is one authority: git spells object ids in LOWERCASE hex, and an uppercase or mixed-case spelling refuses rather than being folded, because folding would make two textual forms name one object and quietly break the round trip. A caller holding a foreign-cased id normalizes deliberately at its own boundary."

fn git_sha1_object_id(hex: String) -> GitObjectId? {
match sha1_hex_digest(hex: hex) {
Expand Down Expand Up @@ -676,60 +677,13 @@ fn git_object_type_wire_name(object_type: ObjectType) -> String {
}
}

fn git_lower_hex_nibble(cp: Int) -> Int? {
if cp >= 48 && cp <= 57 {
Present { value: cp - 48 }
} else if cp >= 97 && cp <= 102 {
Present { value: cp - 87 }
} else {
none
}
}

fn git_octet_of_int(value: Int) -> UInt8 {
value
}

fn git_decode_lower_hex_octets(text: String) -> List<UInt8>? {
if text.length() == 0 {
Present { value: [] }
} else if text.length() < 2 {
none
} else {
match git_lower_hex_nibble(
cp: code_point(substring(s: text, start: 0, end: 1)),
) {
Absent => none
Present { value: high } =>
match git_lower_hex_nibble(
cp: code_point(substring(s: text, start: 1, end: 2)),
) {
Absent => none
Present { value: low } =>
match git_decode_lower_hex_octets(
text: substring(s: text, start: 2, end: text.length()),
) {
Absent => none
Present { value: rest } =>
Present {
value: append(
[git_octet_of_int(value: high * 16 + low)],
items: rest,
)
}
}
}
}
}
}

fn git_object_id_raw_octets(oid: GitObjectId) -> List<UInt8>? {
let text = git_object_id_wire_hex(oid: oid) as String
let expected = git_object_id_raw_size(oid: oid)
if text.length() != expected * 2 {
none
} else {
match git_decode_lower_hex_octets(text: text) {
match base16_decode_lower(hex: text) {
Present { value: octets } =>
if count(octets) == expected {
Present { value: octets }
Expand Down
55 changes: 3 additions & 52 deletions dag/extdeps/mercurial.dag
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import std.graph {
graph_reverse_reachable_members,
}
import extdeps.external_authority { ExternalAuthority }
import extdeps.numeric.base16 { byte_hex_lower }
import extdeps.numeric.base16 { base16_encode_lower, base16_decode_lower }
import extdeps.uri { Uri, Https }

data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
Expand Down Expand Up @@ -119,53 +119,6 @@ type MercurialNodeIdentityDecodeOutcome
cause: MercurialNodeIdentitySyntaxRefusal,
}

fn mercurial_lower_hex_nibble(cp: Int) -> Int? {
if cp >= 48 && cp <= 57 {
Present { value: cp - 48 }
} else if cp >= 97 && cp <= 102 {
Present { value: cp - 87 }
} else {
none
}
}

fn mercurial_octet_of_int(value: Int) -> UInt8 {
value
}

fn mercurial_decode_lower_hex_octets(text: String) -> List<UInt8>? {
if text.length() == 0 {
Present { value: [] }
} else if text.length() < 2 {
none
} else {
match mercurial_lower_hex_nibble(
cp: code_point(substring(s: text, start: 0, end: 1)),
) {
Absent => none
Present { value: high } =>
match mercurial_lower_hex_nibble(
cp: code_point(substring(s: text, start: 1, end: 2)),
) {
Absent => none
Present { value: low } =>
match mercurial_decode_lower_hex_octets(
text: substring(s: text, start: 2, end: text.length()),
) {
Absent => none
Present { value: rest } =>
Present {
value: append(
[mercurial_octet_of_int(value: high * 16 + low)],
items: rest,
)
}
}
}
}
}
}

fn mercurial_node_id_from_octets(
octets: List<UInt8>,
) -> MercurialNodeId {
Expand Down Expand Up @@ -223,9 +176,7 @@ fn mercurial_node_id_octets(
fn mercurial_node_id_text(
node: MercurialNodeId,
) -> String {
mercurial_node_id_octets(node: node)
|> map(octet => byte_hex_lower(v: octet))
|> join(separator: "")
base16_encode_lower(octets: mercurial_node_id_octets(node: node))
}

fn mercurial_decode_node_identity(
Expand All @@ -251,7 +202,7 @@ fn mercurial_decode_node_identity(
cause: MercurialNodeIdentityNonCanonicalUppercase,
}
} else {
match mercurial_decode_lower_hex_octets(text: text) {
match base16_decode_lower(hex: text) {
Absent => MercurialNodeIdentityDecodeRefused {
cause: MercurialNodeIdentityNonHexadecimal,
}
Expand Down
10 changes: 3 additions & 7 deletions dag/extdeps/npm.dag
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import std.content_hash {
import std.decl_ref { DeclarationRef, WholeDeclaration }
import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef }
import extdeps.uri { Uri, Https }
import extdeps.numeric.base16 { byte_hex_lower }
import extdeps.numeric.base16 { base16_encode_lower }
import extdeps.version { VersionIdentity }
import extdeps.pin { Pin, ExactPin }
import extdeps.package { PackageCoordinate, PackageLockNode, PackageLockGraph }
Expand Down Expand Up @@ -265,10 +265,6 @@ fn npm_registry_integrity_from_decode(
}
}

fn npm_octets_to_lower_hex(octets: List<UInt8>) -> String {
map(octets, octet => byte_hex_lower(v: octet)) |> join(separator: "")
}

fn npm_sri_base64_payload_is_canonical(encoded: String, octets: List<UInt8>) -> Bool {
encoded == base64_encode(octets: octets, variant: Standard)
}
Expand All @@ -282,7 +278,7 @@ fn npm_decode_integrity_expression(expression: String) -> NpmIntegrityDecode {
Present { value: octets } =>
if count(octets) == 64 {
if npm_sri_base64_payload_is_canonical(encoded: encoded, octets: octets) {
match sha512_hex_digest(hex: npm_octets_to_lower_hex(octets: octets)) {
match sha512_hex_digest(hex: base16_encode_lower(octets: octets)) {
Present { value: digest } => NpmSingleSha512Integrity { digest: digest }
Absent =>
NpmIntegrityMalformed {
Expand Down Expand Up @@ -310,7 +306,7 @@ fn npm_decode_integrity_expression(expression: String) -> NpmIntegrityDecode {
Present { value: octets } =>
if count(octets) == 20 {
if npm_sri_base64_payload_is_canonical(encoded: encoded, octets: octets) {
match sha1_hex_digest(hex: npm_octets_to_lower_hex(octets: octets)) {
match sha1_hex_digest(hex: base16_encode_lower(octets: octets)) {
Present { value: digest } => NpmSingleSha1Integrity { digest: digest }
Absent =>
NpmIntegrityMalformed {
Expand Down
104 changes: 104 additions & 0 deletions dag/test/claim/base16_rfc4648_witness_test.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
module test.claim.base16_rfc4648_witness_test

import std.integer { UInt8 }
import extdeps.numeric.base16 { base16_encode_lower, base16_decode_lower }
import extdeps.git.object_store { git_sha1_object_id, git_object_id_raw_octets }
import extdeps.mercurial {
mercurial_decode_node_identity,
mercurial_node_id_octets,
mercurial_node_id_text,
MercurialNodeIdentityObservation,
MercurialChangesetNodeIdentity,
MercurialNodeIdentityDecoded,
}
import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly }

data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly

// extdeps.numeric.base16 is the one octet <-> lowercase-hex codec; git and mercurial decoded and
// npm encoded through private copies of it until ENCODING-0 (gunbc#11629) deleted them. The first
// group pins the authority against independent fixed vectors (RFC 4648 section 10, plus octets
// whose nibbles differ so a swapped high/low half cannot pass). The second group is the bypass
// discriminator: each consumer is asked a question only the authority answers, on an asymmetric
// vector, so mutating base16_decode_lower or base16_encode_lower turns these RED -- a consumer that
// had kept its own codec would stay green under that mutation.

data asymmetric_octets: List<UInt8> = [1, 35, 69, 103, 137, 171, 205, 239, 0, 255]

data asymmetric_hex: String = "0123456789abcdef00ff"

data asymmetric_node_hex: String = "0123456789abcdef0123456789abcdef01234567"

data asymmetric_node_octets: List<UInt8> = [1, 35, 69, 103, 137, 171, 205, 239, 1, 35, 69, 103, 137, 171, 205, 239, 1, 35, 69, 103]

test fn base16_encodes_rfc4648_foobar_vector() -> Bool {
base16_encode_lower(octets: [102, 111, 111, 98, 97, 114]) == "666f6f626172"
}

test fn base16_encodes_empty_as_empty() -> Bool {
base16_encode_lower(octets: []) == ""
}

test fn base16_encodes_asymmetric_vector() -> Bool {
base16_encode_lower(octets: asymmetric_octets) == asymmetric_hex
}

test fn base16_decodes_asymmetric_vector() -> Bool {
match base16_decode_lower(hex: asymmetric_hex) {
Present { value: octets } => octets == asymmetric_octets
Absent => false
}
}

test fn base16_decodes_empty_as_empty() -> Bool {
match base16_decode_lower(hex: "") {
Present { value: octets } => octets == []
Absent => false
}
}

test fn base16_refuses_odd_length() -> Bool {
match base16_decode_lower(hex: "abc") {
Present { value: _ } => false
Absent => true
}
}

test fn base16_refuses_uppercase_as_noncanonical() -> Bool {
match base16_decode_lower(hex: "AB") {
Present { value: _ } => false
Absent => true
}
}

test fn base16_refuses_non_hex_digit() -> Bool {
match base16_decode_lower(hex: "0g") {
Present { value: _ } => false
Absent => true
}
}

test fn git_raw_octets_route_through_base16() -> Bool {
match git_sha1_object_id(hex: asymmetric_node_hex) {
Absent => false
Present { value: oid } =>
match git_object_id_raw_octets(oid: oid) {
Present { value: octets } => octets == asymmetric_node_octets
Absent => false
}
}
}

test fn mercurial_node_identity_routes_through_base16() -> Bool {
match mercurial_decode_node_identity(
observation: MercurialNodeIdentityObservation {
role: MercurialChangesetNodeIdentity,
text: asymmetric_node_hex,
},
) {
MercurialNodeIdentityDecoded { node } =>
mercurial_node_id_octets(node: node) == asymmetric_node_octets
&& mercurial_node_id_text(node: node) == asymmetric_node_hex
_ => false
}
}
Loading