package rdfimport ()// ErrCanonComplexity is returned by the canonicalization functions when a graph's// blank-node structure would require more work than maxCanonWork permits — the// guard against the algorithm's exponential worst case on adversarial "poison"// datasets. Well-formed data never approaches the limit.varErrCanonComplexity = errors.New("rdf: canonicalization exceeded complexity budget")// maxCanonWork bounds the total permutations examined across the n-degree hashing// recursion, and maxCanonDepth bounds that recursion's depth. Real datasets finish// in near-linear work at shallow depth (the whole W3C rdf-canon suite peaks at// ~2880 permutations and depth 6); only graphs crafted to be maximally symmetric —// or a long chain of indistinguishable blank nodes, which would otherwise recurse// one frame per link and overflow the goroutine stack — approach these limits, and// are rejected quickly with ErrCanonComplexity rather than hanging or crashing.const ( maxCanonWork = 1 << 18 maxCanonDepth = 1 << 10)// Canonical returns the dataset serialized as canonical N-Quads per the W3C RDF// Dataset Canonicalization algorithm RDFC-1.0 (URDNA2015): blank nodes are// relabeled to isomorphism-invariant _:c14nN identifiers and the quads are sorted,// so two datasets that differ only in statement order or blank-node labels produce// byte-identical output. It returns ErrCanonComplexity for a graph whose symmetry// exceeds the work budget.func ( *Dataset) () ([]byte, error) { , , := canonicalize(.Quads)return , }// Canonicalize returns a new Dataset with blank nodes relabeled to their canonical// _:c14nN identifiers and quads in canonical order, leaving d untouched.func ( *Dataset) (*Dataset, error) { , , := canonicalize(.Quads)if != nil {returnnil, }return &Dataset{Quads: }, nil}// Canonical returns the graph as canonical N-Quads (its triples in the default// graph), per RDFC-1.0. See Dataset.Canonical.func ( *Graph) () ([]byte, error) { := make([]Quad, len(.Triples))for , := range .Triples { [] = Quad{S: .S, P: .P, O: .O} } , , := canonicalize()return , }// identifierIssuer issues canonical or temporary blank-node labels (c14n0, c14n1,// … or b0, b1, …) in first-issue order, per RDFC-1.0's Issue Identifier algorithm.type identifierIssuer struct { prefix string issued map[string]string order []string}func newIssuer( string) *identifierIssuer {return &identifierIssuer{prefix: , issued: map[string]string{}}}func ( *identifierIssuer) () *identifierIssuer { := &identifierIssuer{prefix: .prefix,issued: make(map[string]string, len(.issued)),order: append([]string(nil), .order...), }maps.Copy(.issued, .issued)return}// issue returns the label for id, minting a new one on first sight.func ( *identifierIssuer) ( string) string {if , := .issued[]; {return } := .prefix + strconv.Itoa(len(.order)) .issued[] = .order = append(.order, )return}func ( *identifierIssuer) ( string) (string, bool) { , := .issued[]; return , }type canonicalizer struct { quads []Quad blankQuads map[string][]int// blank id -> indices of quads it occurs in (once per occurrence) canonical *identifierIssuer firstDegree map[string]string// memoized first-degree hashes (a pure function of the quad set) work int}// spend charges n units of work against the complexity budget, panicking with// ErrCanonComplexity when the budget is exhausted. Charges are proportional to the// real cost of a step (quads serialized, issuer entries cloned) so an adversarial// graph that is cheap per permutation but expensive per step still fails fast.func ( *canonicalizer) ( int) { .work += if .work > maxCanonWork {panic(ErrCanonComplexity) }}// canonicalize runs RDFC-1.0 over quads, returning the relabeled+sorted quads and// the canonical N-Quads bytes.func canonicalize( []Quad) ( []Quad, []byte, error) { := &canonicalizer{quads: ,blankQuads: map[string][]int{},canonical: newIssuer("c14n"),firstDegree: map[string]string{}, }for , := range {for , := range [3]Term{.S, .O, .G} {if .Kind == Blank { .blankQuads[.Value] = append(.blankQuads[.Value], ) } } }deferfunc() {if := recover(); != nil {if == ErrCanonComplexity { , , = nil, nil, ErrCanonComplexityreturn }panic() } }()// First-degree hashing: group blank nodes by their neighbourhood hash. := map[string][]string{}for := range .blankQuads { := .hashFirstDegree() [] = append([], ) } := make([]string, 0, len())for := range { = append(, ) }sort.Strings()// Unique first-degree hashes get canonical ids immediately, in hash order.var []stringfor , := range {iflen([]) == 1 { .canonical.issue([][0]) } else { = append(, ) } }// Remaining (hash-colliding) blank nodes are disambiguated by n-degree hashing.for , := range {var []ndegreeResultfor , := range [] {if , := .canonical.get(); {continue } := newIssuer("b") .issue() , := .hashNDegree(, , 0) = append(, ndegreeResult{hash: , issuer: }) }sort.Slice(, func(, int) bool { return [].hash < [].hash })for , := range {for , := range .issuer.order { .canonical.issue() } } }return .serialize()}// relabel maps a blank term to its canonical form; non-blank terms pass through.func ( *canonicalizer) ( Term) Term {if .Kind == Blank {if , := .canonical.get(.Value); {returnNewBlank() } }return}// serialize relabels every quad's blank nodes, sorts the quads by their canonical// N-Quads line, and returns both the sorted quads and the concatenated bytes.func ( *canonicalizer) () ([]Quad, []byte, error) {typestruct {Quad []byte } := make([], len(.quads))for , := range .quads { := Quad{S: .relabel(.S), P: .relabel(.P), O: .relabel(.O), G: .relabel(.G)} [] = {: , : canonLine(nil, , canonAppendTerm)} }sort.Slice(, func(, int) bool { returnstring([].) < string([].) }) := make([]Quad, 0, len())var []bytevarstringfor , := range {if > 0 && string(.) == { // an RDF dataset is a set: drop duplicate quadscontinue } = string(.) = append(, .) = append(, ....) }return , , nil}// hashFirstDegree returns the hash of a blank node's immediate neighbourhood: each// of its quads serialized with the node itself as _:a and any other blank as _:z,// sorted and hashed.func ( *canonicalizer) ( string) string {if , := .firstDegree[]; {return } := func( []byte, Term) []byte {if .Kind == Blank {if .Value == {returnappend(, "_:a"...) }returnappend(, "_:z"...) }returncanonAppendTerm(, ) } := make([]string, 0, len(.blankQuads[]))for , := range .blankQuads[] { = append(, string(canonLine(nil, .quads[], ))) }sort.Strings() := sha256.New()for , := range { .Write([]byte()) } := hex.EncodeToString(.Sum(nil)) .firstDegree[] = return}// hashRelated hashes a related blank node's contribution from one quad position,// preferring an already-issued canonical or temporary id and otherwise its// first-degree hash.func ( *canonicalizer) ( string, Quad, *identifierIssuer, byte) string { := []byte{}if != 'g' { = append(, '<') = append(, .P.Value...) = append(, '>') }if , := .canonical.get(); { = append(, "_:"...) = append(, ...) } elseif , := .get(); { = append(, "_:"...) = append(, ...) } else { = append(, .hashFirstDegree()...) } := sha256.Sum256()returnhex.EncodeToString([:])}type ndegreeResult struct { hash string issuer *identifierIssuer}// hashNDegree computes the n-degree hash of a blank node, distinguishing it from// others that share a first-degree hash by exploring its blank-node neighbourhood// (with permutations) — RDFC-1.0's Hash N-Degree Quads.func ( *canonicalizer) ( string, *identifierIssuer, int) (string, *identifierIssuer) {if > maxCanonDepth {panic(ErrCanonComplexity) } := map[string][]string{}for , := range .blankQuads[] { := .quads[] .addRelated(, .S, , , 's', ) .addRelated(, .O, , , 'o', ) .addRelated(, .G, , , 'g', ) }var []byte := make([]string, 0, len())for := range { = append(, ) }sort.Strings()for , := range { = append(, ...)var []bytevar *identifierIssuerpermute([], func( []string) {// Each visit clones the issuer (O(|issued|)) and may serialize quads, so // charge for that real cost rather than a flat unit per permutation -- // otherwise a graph cheap in permutations but expensive per permutation // evades the budget. .spend(len(.issued) + 1) := .clone()var []bytevar []string := falsefor , := range {if , := .canonical.get(); { = append(, "_:"...) = append(, ...) } else {if , := .get(); ! { = append(, ) } = append(, "_:"...) = append(, .issue()...) }if != nil && len() >= len() && string() > string() { = truebreak } }if ! {for , := range { , := .(, , +1) = append(, "_:"...) = append(, .issue()...) = append(, '<') = append(, ...) = append(, '>') = if != nil && len() >= len() && string() > string() { = truebreak } } }if ! && ( == nil || string() < string()) { = = } }) = append(, ...) = } := sha256.Sum256()returnhex.EncodeToString([:]), }// addRelated records a blank node related to bn through one quad component.func ( *canonicalizer) ( string, Term, Quad, *identifierIssuer, byte, map[string][]string) {if .Kind != Blank || .Value == {return } := .hashRelated(.Value, , , ) [] = append([], .Value)}// permute calls visit for every permutation of list (Heap's algorithm). visit must// not retain the slice: it is mutated in place between calls.func permute( []string, func([]string)) {iflen() == 0 { ()return } := append([]string(nil), ...)varfunc( int) = func( int) {if == 1 { ()return }for := range { ( - 1)if %2 == 0 { [], [-1] = [-1], [] } else { [0], [-1] = [-1], [0] } } } (len())}// ---- canonical N-Quads term serialization ----// canonLine appends one quad as a canonical N-Quads line (terms rendered by term),// terminated by " .\n". A zero-value graph term is the default graph (three terms).func canonLine( []byte, Quad, func([]byte, Term) []byte) []byte { = (, .S) = append(, ' ') = (, .P) = append(, ' ') = (, .O)if .G != (Term{}) { = append(, ' ') = (, .G) }returnappend(, ' ', '.', '\n')}// canonAppendTerm writes a term in canonical N-Quads form.func canonAppendTerm( []byte, Term) []byte {switch .Kind {caseIRI: = append(, '<') = appendEscapedIRI(, .Value)returnappend(, '>')caseBlank: = append(, '_', ':')returnappend(, .Value...)default: = append(, '"') = canonAppendLiteral(, .Value) = append(, '"')if .Lang != "" { = append(, '@')returnappend(, .Lang...) }if .Datatype != "" && .Datatype != XSDString { = append(, '^', '^', '<') = appendEscapedIRI(, .Datatype)returnappend(, '>') }return }}// canonAppendLiteral escapes a literal's lexical form for canonical N-Quads: the// named escapes \b \t \n \f \r \" \\, and \u00XX (uppercase) for other C0 controls// and U+007F; all other bytes pass through as UTF-8. It shares the one literal// escaper with the N-Triples serializer (see appendLiteralEscaped), configured for// the RDFC-1.0 profile.func canonAppendLiteral( []byte, string) []byte {returnappendLiteralEscaped(, , literalEscape{namedBF: true, escapeDEL: true})}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.