// RDFC-1.0 (URDNA2015) canonicalization, W3C REC 2023-08-22
// (spec.md section 8.132 route A; suite git anchor 15619df2fda7a4ca88308733789b6774517f9638).
// Six-step main algorithm per section 4.4.3 (main), 4.5.2 (issue
// identifier), 4.6.3 (hash first degree quads), 4.7.3 (hash related
// blank node), 4.8.3 (hash n-degree quads); output serialization per
// appendix A canonical N-Quads. Input parses through the fromRDF face
// N-Quads parser (nquads_parse.mbt); SHA-256 (sha256.mbt) supplies the
// bnode ordering keys as lowercase hex strings. Suite anchors live in
// canon_rdfc10_wbtest.mbt (byte-inlined suite pairs); the full 86-entry
// harness is the next battle (canon_rdfc10_test.mbt).
///|
/// Identifier issuer (section 4.5.2): prefix + counter. `issued` maps
/// existing identifiers to issued ones; `order` preserves issuance order
/// because main step 5.3 replays it in the same order. Issuers are shared
/// by reference through the n-degree recursion (section 4.8.3 step
/// 5.4.5.4), so issue() mutates in place.
priv struct C14nIssuer {
prefix : String
issued : Map[String, String]
order : Array[String]
mut counter : Int
}
///|
/// Issue Identifier (section 4.5.2): an existing mapping wins; a new
/// mapping takes prefix + counter and records its issuance order.
fn C14nIssuer::issue(self : C14nIssuer, existing : String) -> String {
match self.issued.get(existing) {
Some(issued) => issued
None => {
let issued = self.prefix + self.counter.to_string()
self.issued[existing] = issued
self.order.push(existing)
self.counter = self.counter + 1
issued
}
}
}
///|
/// Copy for the per-permutation issuer copies (section 4.8.3 step
/// 5.4.4.1): fresh issued map and order array carrying the same state.
fn C14nIssuer::fresh_copy(self : C14nIssuer) -> C14nIssuer {
let issued : Map[String, String] = Map([])
for existing, mapped in self.issued {
issued[existing] = mapped
}
{
prefix: self.prefix,
issued,
order: self.order.copy(),
counter: self.counter,
}
}
///|
/// Hash algorithm option (REC section 3.1 hashAlgorithm; suite pins the
/// SHA384 variant on test075). Default SHA256.
pub(all) enum C14nHashAlgorithm {
Sha256
Sha384
} derive(Eq)
///|
/// [0079] 消警:Eq 派生的 equal/not_equal 显式升格(const §5——derive 须同笔补
/// extend;役59 漏此条,2026-10-03 deny-warn 门红暴露)
pub extend C14nHashAlgorithm with Eq::{not_equal, equal}
///|
/// Canonicalization state (section 4.4.3 steps 1-2): the parsed quads,
/// the blank node to quads map (label -> quad indices), the canonical
/// issuer (prefix c14n) and a work budget defending the poison clique
/// suite entry (test074c NegativeEvalTest: signal failure instead of
/// exhausting resources; the real high-complexity tests 044-046 stay
/// orders of magnitude below it).
priv struct C14nState {
quads : Array[NqQuad]
bnode_to_quads : Map[String, Array[Int]]
canonical_issuer : C14nIssuer
hash_algorithm : C14nHashAlgorithm
mut work_budget : Int
}
///|
/// Work budget in units of one hash-n-degree call or permutation attempt.
const C14N_WORK_BUDGET : Int = 1000000
///|
/// Steps 1-2: index every quad under each of its blank node components
/// (subject / object / graph name).
fn C14nState::build(
quads : Array[NqQuad],
hash_algorithm : C14nHashAlgorithm,
) -> C14nState {
let unique_quads : Array[NqQuad] = []
let seen : Map[String, Bool] = Map([])
for quad in quads {
let key = c14n_quad_key(quad)
match seen.get(key) {
Some(_) => ()
None => {
seen[key] = true
unique_quads.push(quad)
}
}
}
let bnode_to_quads : Map[String, Array[Int]] = Map([])
for quad_index, quad in unique_quads {
for component in c14n_bnode_components(quad) {
let (label, _position) = component
match bnode_to_quads.get(label) {
Some(indices) => indices.push(quad_index)
None => bnode_to_quads[label] = [quad_index]
}
}
}
{
quads: unique_quads,
bnode_to_quads,
canonical_issuer: {
prefix: "c14n",
issued: Map([]),
order: [],
counter: 0,
},
hash_algorithm,
work_budget: C14N_WORK_BUDGET,
}
}
///|
/// Dedup key for a quad: the four serialized terms with an explicit
/// graph marker (None / Some differs only by arity, never content).
fn c14n_quad_key(quad : NqQuad) -> String {
let raw_terms = fn(term : NqTerm) -> String {
c14n_serialize_term(term, fn(_label) { None })
}
let graph_marker = match quad.graph {
Some(graph) => raw_terms(graph)
None => "-"
}
raw_terms(quad.subject) +
" " +
raw_terms(quad.predicate) +
" " +
raw_terms(quad.object) +
" " +
graph_marker
}
///|
/// Blank node components of a quad in spec order with their position
/// markers (sections 4.6.3/4.8.3: s / o / g; the predicate is always an
/// IRI and never a blank node).
fn c14n_bnode_components(quad : NqQuad) -> Array[(String, String)] {
let components : Array[(String, String)] = []
match quad.subject {
NqBnodeTerm(label) => components.push((label, "s"))
_ => ()
}
match quad.object {
NqBnodeTerm(label) => components.push((label, "o"))
_ => ()
}
match quad.graph {
Some(NqBnodeTerm(label)) => components.push((label, "g"))
_ => ()
}
components
}
///|
/// Code point order comparison (the spec's universal ordering rule):
/// walk both strings as code point sequences; Char::to_int is the code
/// point. (String::compare is length-first — not the spec order.)
fn c14n_compare(first : String, second : String) -> Int {
let first_iter = first.iter()
let second_iter = second.iter()
let mut first_next = first_iter.next()
let mut second_next = second_iter.next()
while first_next != None && second_next != None {
let first_char = first_next.unwrap()
let second_char = second_next.unwrap()
if first_char != second_char {
return first_char.to_int() - second_char.to_int()
}
first_next = first_iter.next()
second_next = second_iter.next()
}
if first_next != None {
1
} else if second_next != None {
-1
} else {
0
}
}
///|
/// Map keys in code point order (the spec's ordered maps all drive their
/// iteration through this: main steps 4-5, section 4.8.3 step 5).
fn c14n_sorted_keys(source : Map[String, Array[String]]) -> Array[String] {
let keys : Array[String] = []
for key, _bucket in source {
keys.push(key)
}
keys.sort_by(fn(first, second) { c14n_compare(first, second) })
keys
}
///|
/// Uppercase hex digit for \uXXXX UCHAR forms (appendix A: HEX digits
/// uppercase; the JCS helper nq_hex_char is lowercase and stays JCS-only).
fn c14n_hex_upper(digit : Int) -> Char {
let value = if digit < 10 { 48 + digit } else { 55 + digit }
Int::unsafe_to_char(value)
}
///|
/// Append one \uXXXX escape with uppercase HEX digits.
fn c14n_write_uchar(out : StringBuilder, point : Int) -> Unit {
out.write_string("\\u")
out.write_char(c14n_hex_upper(point / 4096 % 16))
out.write_char(c14n_hex_upper(point / 256 % 16))
out.write_char(c14n_hex_upper(point / 16 % 16))
out.write_char(c14n_hex_upper(point % 16))
}
///|
/// STRING_LITERAL_QUOTE canonical escaping (appendix A): ECHAR for
/// BS/HT/LF/FF/CR/quote/backslash; UCHAR for the other control characters
/// (0x00-0x1F), DEL (0x7F) and non-Chars (surrogates); every other code
/// point passes raw (UTF-8 on the wire).
fn c14n_escape_string(value : String) -> String {
let out = StringBuilder()
for ch in value {
let point = ch.to_int()
if point == 92 {
out.write_string("\\\\")
} else if point == 34 {
out.write_string("\\\"")
} else if point == 10 {
out.write_string("\\n")
} else if point == 13 {
out.write_string("\\r")
} else if point == 9 {
out.write_string("\\t")
} else if point == 8 {
out.write_string("\\b")
} else if point == 12 {
out.write_string("\\f")
} else if (point >= 0 && point <= 31) ||
point == 127 ||
(point >= 0xD800 && point <= 0xDFFF) {
c14n_write_uchar(out, point)
} else {
out.write_char(ch)
}
}
out.to_string()
}
///|
/// IRI canonical escaping (appendix A): UCHAR only where the IRIREF
/// production forbids the raw character (controls 0x00-0x20, quote, angle
/// brackets, braces, pipe, caret, backtick, backslash); the rest passes
/// raw.
fn c14n_escape_iri(iri : String) -> String {
let out = StringBuilder()
for ch in iri {
let point = ch.to_int()
let forbidden = point <= 32 ||
point == 34 ||
point == 60 ||
point == 62 ||
point == 123 ||
point == 124 ||
point == 125 ||
point == 94 ||
point == 96 ||
point == 92 ||
(point >= 0xD800 && point <= 0xDFFF)
if forbidden {
c14n_write_uchar(out, point)
} else {
out.write_char(ch)
}
}
out.to_string()
}
///|
/// Canonical N-Quads term serialization (appendix A). Blank node labels
/// go through the label override (first-degree _:a/_:z forcing, canonical
/// c14n relabeling, raw passthrough as the total-function fallback);
/// xsd:string datatype is suppressed; langtags lowercase.
fn c14n_serialize_term(term : NqTerm, label : (String) -> String?) -> String {
match term {
NqIriTerm(iri) => "<" + c14n_escape_iri(iri) + ">"
NqBnodeTerm(raw_label) =>
match label(raw_label) {
Some(overridden) => "_:" + overridden
None => "_:" + raw_label
}
NqLitTerm(value, datatype, langtag) => {
let out = StringBuilder()
out.write_string("\"" + c14n_escape_string(value) + "\"")
match langtag {
Some(tag) => out.write_string("@" + nq_lowercase(tag))
None => ()
}
match datatype {
Some(datatype_iri) =>
if datatype_iri != XSD_STRING {
out.write_string("^^<" + c14n_escape_iri(datatype_iri) + ">")
}
None => ()
}
out.to_string()
}
}
}
///|
/// Canonical N-Quads line for one quad (appendix A): terms + optional
/// graph term + " ." — the line feed is part of the form (sorted lines
/// concatenate directly into the canonical document).
fn c14n_quad_line(quad : NqQuad, label : (String) -> String?) -> String {
let out = StringBuilder()
out.write_string(c14n_serialize_term(quad.subject, label))
out.write_string(" ")
out.write_string(c14n_serialize_term(quad.predicate, label))
out.write_string(" ")
out.write_string(c14n_serialize_term(quad.object, label))
match quad.graph {
Some(graph) => {
out.write_string(" ")
out.write_string(c14n_serialize_term(graph, label))
}
None => ()
}
out.write_string(" .\n")
out.to_string()
}
///|
/// Digest dispatch (the single point where the hash algorithm option
/// materializes; every ordering key goes through here).
fn C14nState::digest_utf8_hex(self : C14nState, input : String) -> String {
match self.hash_algorithm {
C14nHashAlgorithm::Sha256 => sha256_utf8_hex(input)
C14nHashAlgorithm::Sha384 => sha384_utf8_hex(input)
}
}
///|
/// Hash First Degree Quads (section 4.6.3): serialize each quad holding
/// the identifier with self forced to _:a and every other blank node to
/// _:z, sort the lines code point order, hash the concatenation.
fn C14nState::hash_first_degree(
self : C14nState,
identifier : String,
) -> String {
let lines : Array[String] = []
match self.bnode_to_quads.get(identifier) {
Some(quad_indices) =>
for quad_index in quad_indices {
let quad = self.quads[quad_index]
let forced_label = fn(label : String) -> String? {
if label == identifier {
Some("a")
} else {
Some("z")
}
}
lines.push(c14n_quad_line(quad, forced_label))
}
None => ()
}
lines.sort_by(fn(first, second) { c14n_compare(first, second) })
let concatenated = StringBuilder()
for line in lines {
concatenated.write_string(line)
}
self.digest_utf8_hex(concatenated.to_string())
}
///|
/// Hash Related Blank Node (section 4.7.3): the position marker, then the
/// predicate (every position except g), then the related identifier as
/// canonical label, temporary label, or — when unissued — its first
/// degree hash verbatim (no label prefix).
fn C14nState::hash_related_blank_node(
self : C14nState,
related : String,
quad : NqQuad,
issuer : C14nIssuer,
position : String,
) -> String {
let input = StringBuilder()
input.write_string(position)
if position != "g" {
let predicate_iri = match quad.predicate {
NqIriTerm(iri) => iri
_ => ""
}
input.write_string("<" + predicate_iri + ">")
}
match self.canonical_issuer.issued.get(related) {
Some(canonical) => input.write_string("_:" + canonical)
None =>
match issuer.issued.get(related) {
Some(temporary) => input.write_string("_:" + temporary)
None => input.write_string(self.hash_first_degree(related))
}
}
self.digest_utf8_hex(input.to_string())
}
///|
/// Next lexicographic permutation in place (Steinhaus-style
/// next-permutation); false when the sequence is at its last arrangement.
/// Duplicate labels collapse to one arrangement each — semantically
/// equivalent (equal paths never displace the chosen one) and cheaper.
fn c14n_next_permutation(items : Array[String]) -> Bool {
let length = items.length()
let mut pivot = length - 1
while pivot > 0 && c14n_compare(items[pivot - 1], items[pivot]) >= 0 {
pivot = pivot - 1
}
if pivot == 0 {
return false
}
let mut successor = length - 1
while c14n_compare(items[successor], items[pivot - 1]) <= 0 {
successor = successor - 1
}
let swapped = items[pivot - 1]
items[pivot - 1] = items[successor]
items[successor] = swapped
let mut left = pivot
let mut right = length - 1
while left < right {
let reversed = items[left]
items[left] = items[right]
items[right] = reversed
left = left + 1
right = right - 1
}
true
}
///|
/// Hash N-Degree Quads (section 4.8.3). Mutates path_issuer in place (the
/// recursion shares the permutation's issuer copy by reference, step
/// 5.4.5.4) and returns the hash together with the possibly extended
/// issuer. The work budget gates every recursive call and permutation
/// attempt; an exhausted budget yields an empty hash that the main loop
/// converts into JsonLdError (poison clique, suite test074c).
fn C14nState::hash_n_degree(
self : C14nState,
identifier : String,
path_issuer : C14nIssuer,
) -> (String, C14nIssuer) {
if self.work_budget <= 0 {
return ("", path_issuer)
}
self.work_budget = self.work_budget - 1
let hash_to_bnodes : Map[String, Array[String]] = Map([])
match self.bnode_to_quads.get(identifier) {
Some(quad_indices) =>
for quad_index in quad_indices {
let quad = self.quads[quad_index]
for component in c14n_bnode_components(quad) {
let (related, position) = component
if related != identifier {
let related_hash = self.hash_related_blank_node(
related, quad, path_issuer, position,
)
match hash_to_bnodes.get(related_hash) {
Some(bucket) => bucket.push(related)
None => hash_to_bnodes[related_hash] = [related]
}
}
}
}
None => ()
}
let data_to_hash = StringBuilder()
let mut issuer = path_issuer
for related_hash in c14n_sorted_keys(hash_to_bnodes) {
data_to_hash.write_string(related_hash)
let blank_node_list = hash_to_bnodes.get(related_hash).unwrap()
let working_list = blank_node_list.copy()
working_list.sort_by(fn(first, second) { c14n_compare(first, second) })
let mut chosen_path = ""
let mut chosen_issuer : C14nIssuer? = None
let mut budget_exhausted = false
let mut enumeration_done = false
while !enumeration_done {
if self.work_budget <= 0 {
budget_exhausted = true
enumeration_done = true
continue
}
self.work_budget = self.work_budget - 1
let mut issuer_copy = issuer.fresh_copy()
let mut path = ""
let recursion_list : Array[String] = []
let mut pruned = false
for related in working_list {
match self.canonical_issuer.issued.get(related) {
Some(canonical) => path = path + "_:" + canonical
None => {
if !issuer_copy.issued.contains(related) {
recursion_list.push(related)
}
let temporary = issuer_copy.issue(related)
path = path + "_:" + temporary
}
}
if chosen_path != "" &&
path.length() >= chosen_path.length() &&
c14n_compare(path, chosen_path) > 0 {
pruned = true
break
}
}
if !pruned {
for related in recursion_list {
let (result_hash, result_issuer) = self.hash_n_degree(
related, issuer_copy,
)
let already_issued = issuer_copy.issue(related)
path = path + "_:" + already_issued + "<" + result_hash + ">"
issuer_copy = result_issuer
if chosen_path != "" &&
path.length() >= chosen_path.length() &&
c14n_compare(path, chosen_path) > 0 {
pruned = true
break
}
}
}
if !pruned {
if chosen_path == "" || c14n_compare(path, chosen_path) < 0 {
chosen_path = path
chosen_issuer = Some(issuer_copy)
}
}
if !c14n_next_permutation(working_list) {
enumeration_done = true
}
}
if budget_exhausted {
return ("", issuer)
}
data_to_hash.write_string(chosen_path)
match chosen_issuer {
Some(next_issuer) => issuer = next_issuer
None => ()
}
}
(self.digest_utf8_hex(data_to_hash.to_string()), issuer)
}
///|
/// Main canonicalization loop (section 4.4.3 steps 3-7): first degree
/// hashes group the blank nodes (step 3); unique-hash nodes take
/// canonical labels immediately (step 4); each remaining group explores
/// n-degree hash paths (step 5) and replays every temporary issuer's
/// order into the canonical issuer; finally all quads serialize under
/// canonical labels and sort code point order (steps 6-7).
fn C14nState::run(
self : C14nState,
) -> Result[(String, Map[String, String]), JsonLdError] {
let hash_to_bnodes : Map[String, Array[String]] = Map([])
for label, _indices in self.bnode_to_quads {
let first_degree = self.hash_first_degree(label)
match hash_to_bnodes.get(first_degree) {
Some(bucket) => bucket.push(label)
None => hash_to_bnodes[first_degree] = [label]
}
}
for first_degree in c14n_sorted_keys(hash_to_bnodes) {
let bucket = hash_to_bnodes.get(first_degree).unwrap()
if bucket.length() == 1 {
let _issued = self.canonical_issuer.issue(bucket[0])
let _removed = hash_to_bnodes.remove(first_degree)
}
}
let mut poisoned = false
for first_degree in c14n_sorted_keys(hash_to_bnodes) {
let identifier_list = hash_to_bnodes.get(first_degree).unwrap()
let hash_path_list : Array[(String, C14nIssuer, Int)] = []
for n in identifier_list {
match self.canonical_issuer.issued.get(n) {
Some(_) => ()
None => {
let temporary_issuer : C14nIssuer = {
prefix: "b",
issued: Map([]),
order: [],
counter: 0,
}
let _temporary = temporary_issuer.issue(n)
let (result_hash, result_issuer) = self.hash_n_degree(
n, temporary_issuer,
)
hash_path_list.push(
(result_hash, result_issuer, hash_path_list.length()),
)
}
}
}
hash_path_list.sort_by(fn(first, second) {
let by_hash = c14n_compare(first.0, second.0)
if by_hash != 0 {
by_hash
} else {
first.2 - second.2
}
})
for entry in hash_path_list {
let result_issuer = entry.1
for existing in result_issuer.order {
let _canonical = self.canonical_issuer.issue(existing)
}
}
if self.work_budget <= 0 {
poisoned = true
}
}
if poisoned {
return Err(
JsonLdError::Unsupported(
"RDFC-1.0 canonicalization aborted: work budget exhausted (poison graph)",
),
)
}
let lines : Array[String] = []
for quad in self.quads {
let canonical_label = fn(label : String) -> String? {
self.canonical_issuer.issued.get(label)
}
lines.push(c14n_quad_line(quad, canonical_label))
}
lines.sort_by(fn(first, second) { c14n_compare(first, second) })
let canonical_form = StringBuilder()
for line in lines {
canonical_form.write_string(line)
}
Ok((canonical_form.to_string(), self.canonical_issuer.issued))
}
///|
/// Full-option entry (REC section 3.1): the hash algorithm selects the
/// SHA384 variant pinned by suite test075. Output shape matches the
/// mapping variant.
///
/// **定位改判(役65 库化整形)**:本入口=RDFC-1.0 规范入口(库特征,
/// 非 harness 专用)——hash 算法可配(SHA-256/384),返回规范形 + bnode
/// 映射,是 RDFC-1.0 对外能力的直出口。原「非通用 API 承诺」(役62)随
/// 库定位拍板解除;包内其余入口(`rdfc10_canonicalize` / `_with_mapping` /
/// `sha256_hex` / `sha256_utf8_hex` / `sha384_hex` / `sha384_utf8_hex`)
/// 真调用点逐处 grep 定性,役62 已收 `priv`。
pub fn rdfc10_canonicalize_with_hash(
input_nquads : String,
hash_algorithm : C14nHashAlgorithm,
) -> Result[(String, Map[String, String]), JsonLdError] {
match nq2_parse(input_nquads) {
Ok(quads) => C14nState::build(quads, hash_algorithm).run()
Err(message) => Err(JsonLdError::Syntax(message))
}
}