From 43301b47e5643de823fc14c51e4826dde54db3fb Mon Sep 17 00:00:00 2001 From: Richard Levitte Date: Tue, 12 Aug 2025 15:00:01 +0200 Subject: [PATCH] RIP 4: URI Scheme Building on the work that Lorenz Leutgeb did in patch rad:z3trNYnLWS11cJWC6BbxDs5niGo82/patch/19ba16634fbde97156255ed97069d0aa3ab2bf12, the URI scheme is brought to a final RIP document, with full ABNF and Nearley grammars as well as examples, and a couple of scripts to extract and test those grammars against those examples. Compared to the URL scheme in RIP 3, this RIP brings a subtly different URL form, while also supporting the RIP 3 scheme (now considered legacy). In short, these two forms are supported by this RIP: - rad://{rid}/{nid} (considered legacy) - rad://{rad-auth}/{rid}[/{namespace}][/{resource-type-and-identifier}] (rad-auth is the authority part, which can take the form of {nid}@{host-and-port} or parts thereof) This works, because the {rid} and the {nid} are distinguishable, as a {nid} always starts with z6Mk, while a {rid} does start with a z, but never with z6Mk, and they have different lengths (a {rid} is always 28 or 29 characters long, which a {nid} is always 48 characters long). Co-authored-by: Fintan Halpenny Co-authored-by: Lorenz Leutgeb --- .gitignore | 1 + 0004-general-uri-scheme/data/rad-uri.abnf | 208 +++++ 0004-general-uri-scheme/data/rad-uri.ne | 245 +++++ 0004-general-uri-scheme/data/test/tc00a | 1 + .../data/test/tc00a-expected.json | 6 + 0004-general-uri-scheme/data/test/tc00b | 1 + .../data/test/tc00b-expected.json | 6 + 0004-general-uri-scheme/data/test/tc00c | 1 + .../data/test/tc00c-expected.json | 6 + 0004-general-uri-scheme/data/test/tc01a | 1 + .../data/test/tc01a-expected.json | 7 + 0004-general-uri-scheme/data/test/tc01b | 1 + .../data/test/tc01b-expected.json | 7 + 0004-general-uri-scheme/data/test/tc01c | 1 + .../data/test/tc01c-expected.json | 7 + 0004-general-uri-scheme/data/test/tc02 | 1 + .../data/test/tc02-expected.json | 7 + 0004-general-uri-scheme/data/test/tc03 | 1 + .../data/test/tc03-expected.json | 8 + 0004-general-uri-scheme/data/test/tc04 | 1 + .../data/test/tc04-expected.json | 8 + 0004-general-uri-scheme/data/test/tc05 | 1 + .../data/test/tc05-expected.json | 9 + 0004-general-uri-scheme/data/test/tc10a | 1 + .../data/test/tc10a-expected.json | 18 + 0004-general-uri-scheme/data/test/tc10b | 1 + .../data/test/tc10b-expected.json | 18 + 0004-general-uri-scheme/data/test/tc12 | 1 + .../data/test/tc12-expected.json | 20 + 0004-general-uri-scheme/data/test/tc13 | 1 + .../data/test/tc13-expected.json | 24 + 0004-general-uri-scheme/data/test/tc30 | 1 + .../data/test/tc30-expected.json | 13 + 0004-general-uri-scheme/data/test/tc31 | 1 + .../data/test/tc31-expected.json | 14 + 0004-general-uri-scheme/data/test/tc32 | 1 + .../data/test/tc32-expected.json | 12 + 0004-general-uri-scheme/data/test/tc40a | 1 + .../data/test/tc40a-expected.json | 15 + 0004-general-uri-scheme/data/test/tc40b | 1 + .../data/test/tc40b-expected.json | 15 + 0004-general-uri-scheme/data/test/tc41a | 1 + .../data/test/tc41a-expected.json | 15 + 0004-general-uri-scheme/data/test/tc41b | 1 + .../data/test/tc41b-expected.json | 15 + 0004-general-uri-scheme/data/test/tc42a | 1 + .../data/test/tc42a-expected.json | 15 + 0004-general-uri-scheme/data/test/tc42b | 1 + .../data/test/tc42b-expected.json | 15 + 0004-general-uri-scheme/data/test/tc43a | 1 + .../data/test/tc43a-expected.json | 16 + 0004-general-uri-scheme/data/test/tc43b | 1 + .../data/test/tc43b-expected.json | 16 + 0004-general-uri-scheme/data/test/tc44 | 1 + .../data/test/tc44-expected.json | 15 + .../data/test/uri-scheme-tester.sh | 42 + .../general-uri-scheme.adoc | 867 ++++++++++++++++++ flake.nix | 37 +- nix/nearley/package-lock.json | 87 ++ nix/nearley/package.json | 11 + 60 files changed, 1848 insertions(+), 3 deletions(-) create mode 100644 0004-general-uri-scheme/data/rad-uri.abnf create mode 100644 0004-general-uri-scheme/data/rad-uri.ne create mode 100644 0004-general-uri-scheme/data/test/tc00a create mode 100644 0004-general-uri-scheme/data/test/tc00a-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc00b create mode 100644 0004-general-uri-scheme/data/test/tc00b-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc00c create mode 100644 0004-general-uri-scheme/data/test/tc00c-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc01a create mode 100644 0004-general-uri-scheme/data/test/tc01a-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc01b create mode 100644 0004-general-uri-scheme/data/test/tc01b-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc01c create mode 100644 0004-general-uri-scheme/data/test/tc01c-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc02 create mode 100644 0004-general-uri-scheme/data/test/tc02-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc03 create mode 100644 0004-general-uri-scheme/data/test/tc03-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc04 create mode 100644 0004-general-uri-scheme/data/test/tc04-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc05 create mode 100644 0004-general-uri-scheme/data/test/tc05-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc10a create mode 100644 0004-general-uri-scheme/data/test/tc10a-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc10b create mode 100644 0004-general-uri-scheme/data/test/tc10b-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc12 create mode 100644 0004-general-uri-scheme/data/test/tc12-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc13 create mode 100644 0004-general-uri-scheme/data/test/tc13-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc30 create mode 100644 0004-general-uri-scheme/data/test/tc30-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc31 create mode 100644 0004-general-uri-scheme/data/test/tc31-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc32 create mode 100644 0004-general-uri-scheme/data/test/tc32-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc40a create mode 100644 0004-general-uri-scheme/data/test/tc40a-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc40b create mode 100644 0004-general-uri-scheme/data/test/tc40b-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc41a create mode 100644 0004-general-uri-scheme/data/test/tc41a-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc41b create mode 100644 0004-general-uri-scheme/data/test/tc41b-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc42a create mode 100644 0004-general-uri-scheme/data/test/tc42a-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc42b create mode 100644 0004-general-uri-scheme/data/test/tc42b-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc43a create mode 100644 0004-general-uri-scheme/data/test/tc43a-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc43b create mode 100644 0004-general-uri-scheme/data/test/tc43b-expected.json create mode 100644 0004-general-uri-scheme/data/test/tc44 create mode 100644 0004-general-uri-scheme/data/test/tc44-expected.json create mode 100755 0004-general-uri-scheme/data/test/uri-scheme-tester.sh create mode 100644 0004-general-uri-scheme/general-uri-scheme.adoc create mode 100644 nix/nearley/package-lock.json create mode 100644 nix/nearley/package.json diff --git a/.gitignore b/.gitignore index b2be92b..8583d22 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +node_modules/ result diff --git a/0004-general-uri-scheme/data/rad-uri.abnf b/0004-general-uri-scheme/data/rad-uri.abnf new file mode 100644 index 0000000..53b0da3 --- /dev/null +++ b/0004-general-uri-scheme/data/rad-uri.abnf @@ -0,0 +1,208 @@ +; This grammar is written using ABNF, as defined in RFC 5234 +; (https://datatracker.ietf.org/doc/html/rfc5234), from which ALPHA, DIGIT +; and HEXDIG are borrowed +; +; Furthermore, this grammar is based on and specialises the generic URI +; specification (https://datatracker.ietf.org/doc/html/rfc3986) for Radicle, +; from which 'query', 'fragment', 'host', 'port', 'unreserved', 'pchar', ... +; are borrowed + +; tag::overview[] +; RFC 3986 defines the syntax for 'query' and 'fragment' +rad = rad-scheme ":" rad-auth-and-resource rad-resource-type-and-identity [ "?" query ] [ "#" fragment ] + / rad-scheme ":" rad-legacy + +rad-scheme = "rad" + +; RFC 3986 allows an absolute resource path without authority, i.e. a URI +; that looks like this: rad:/{RID}/{NID} +; We don't include that sort of URI in this scheme, because it's, strictly +; speaking, unnecessary in this context, and may seem confusing. +rad-auth-and-resource = "//" [ rad-auth ] "/" rad-resource ; abs. resource "path" with authority + / rad-resource ; rel. resource "path" + +; rad-legacy are URLs defined by RIP #3. That URL doesn't quite hold up +; against the general URI syntax defined in RFC3986, in so far that a node +; (identified with NID or NID@host) is the conceptual authority, not +; a repository. +; To be noted is that RIDs and NIDs are distinct enough that there should not +; be any ambiguity between rad-auth-and-resource and rad-legacy. +; Of all current implementations, this mostly impacts the git remote helper +; git-remote-rad. +rad-legacy = "//" rad-resource +; end::overview[] + +; The NID is conceptually ambiguous from an RFC 3986 syntax perspective: +; +; authority = [ userinfo "@" ] host [ ":" port ] +; +; Is the NID 'userinfo' or 'host'? In a sense, it currently is a little bit +; of both. Furthermore, Radicle requires that if a hostname (+ port) is +; given, the NID must be given too. Therefore, instead of using 'authority' +; strictly as given by RFC 3986, we allow ourselves a slightly different +; syntax, which is fine. Do note also, that a 'host' can still be made to +; look like an NID, so programs dealing directly with the Radicle network +; will have to be prepared to handle that ambiguity appropriately. +; tag::authority[] +; +; TODO: There already are ways to resolve its NID from a hostname. +; For example, if `radicle-httpd` runs next to the node, then it can be +; queried for its NID via HTTPS. Also, there are ways to use DNS TXT records, +; which is also done for DNS-SD. +; We would therefore likely make the `rad-node` optional, or even omit it. +; If `rad-node` is sufficiently different from a DNS name (it probably is +; not), then we can even omit "@". +rad-auth = rad-node [ "@" rad-host-and-port ] + +; TODO: Port might be optional in the future, once we have convinced IANA +; to assign one to us. There currently is no default port. +rad-host-and-port = host ":" port + +; rad-node is a semantic symbol, to signify intent +rad-node = rad-nid + +; An NID is an ID that is ultimately a public key in the Radicle network. +; This is used to uniquely represent a node, or a user (also a node), or a +; namespace within a repository. +; It is a [multibase] and [multicodec] encoded string, using [base58btc] as the multibase +; and the codec for the underlying Ed25519 public key. +; +; It is always 48 characters long, including the initial "z6Mk", +; the `z` corresponding to the base, and `6Mk` corresponding to the codec. +; +; [base58btc]: https://digitalbazaar.github.io/base58-spec/ +; [multibase]: https://www.ietf.org/archive/id/draft-multiformats-multibase-07.html +; [multicodec]: https://github.com/multiformats/multicodec/ +; +; TODO: New NIDs in the future? Even this might change, as we allow rotating +; keys for nodes. But this is even further out than stable IDs for users. +rad-nid = "z6Mk" 44( base58btc ) +; end::authority[] + +; tag::resource[] +rad-resource = rad-repository [ "/" rad-namespace ] + +; rad-repository is a semantic symbol, to signify intent +rad-repository = rad-rid + +; rad-namespace is a semantic symbol, to signify intent +rad-namespace = rad-nid + +; An RID uniquely represents a git repository in the Radicle network. +; It is a [multibase] encoded string of a Git OID, using [base58btc] as the multibase. +; +; It is 28 or 29 characters long, including the initial multibase identifier "z". +; +; [base58btc]: https://digitalbazaar.github.io/base58-spec/ +; [multibase]: https://www.ietf.org/archive/id/draft-multiformats-multibase-07.html +; +; TODO: In the near future (as soon as Git repositories with SHA-256 object +; format become popular, which will be in late 2026), we will have to extend +; the syntax of RIDs. Leave a note here so that implementors know this and +; make design decisions accordingly. +rad-rid = "z" 27*28( base58btc ) +; end::resource[] + +; tag::resource-type-and-identity[] +; 'rad-resource-type-and-identity' is empty by default, and more resource +; types and identities will be added further on, using the ABNF =/ operator, +; see RFC 5234 "3.3. Incremental Alternatives: Rule1 =/ Rule2" +rad-resource-type-and-identity = "" + +; tag::git-type-and-identity[] +; For git specific resource types +rad-resource-type-and-identity =/ git-commit / git-tree / git-blob / git-tag + +git-commit = "/" "commit" "/" git-obj-or-ref +git-tree = "/" "tree" "/" git-obj +git-blob = "/" "blob" "/" git-obj +git-tag = "/" "tag" "/" git-obj-or-ref + +; Note: this rule will always be ambiguous, as something that looks like a +; git object ID could very well be a branch or a tag. +; Some git commands will warn when such refs are used, but not all. +git-obj-or-ref = git-obj / git-ref +; Currently, only sha1 identities are supported. +git-obj = git-sha1 +; HEXDIG is defined in RFC 5234 +git-sha1 = 40HEXDIG + +; unreserved is defined in RFC 3986. Not that this syntax allows much +; more than what can be specified for a git reference. It's left to +; the implementation to parse and apply semantics properly. +; Our best reference for how to interpret and parse git references is +; https://git-scm.com/docs/protocol-common.html +git-ref = 1*unreserved *( "/" 1*unreserved ) +; end::git-type-and-identity[] + +; tag::rad-cob-type-and-identity[] +; For Radicle collaborative object resource types +rad-resource-type-and-identity =/ rad-cob / rad-cobs + +rad-cob = "/" "cob" "/" rad-cob-type "/" rad-cob-obj +rad-cobs = "/" "cob" "/" rad-cob-type + +; The COB type is like a reversed FQDN. The syntax spec is derived from +; https://datatracker.ietf.org/doc/html/rfc1034#section-3.5 +rad-cob-type = rad-cob-label 1*( "." rad-cob-label ) +rad-cob-label = 1*( ALPHA / DIGIT ) *( "-" 1*( ALPHA / DIGIT ) ) + +; syntactically, radicle COB objects and object revisions are git objects +; semantically, they are as well, at least for now +rad-cob-obj = git-obj +; end::rad-cob-type-and-identity[] +; end::resource-type-and-identity[] + +; tag::rfc5234[] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Copied from RFC 5234 + +ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +DIGIT = %x30-39 ; 0-9 +HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" + +; Copied from RFC 3986 +host = IP-literal / IPv4address / reg-name +port = *DIGIT + +IP-literal = "[" ( IPv6address / IPvFuture ) "]" + +IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + +IPv6address = 6( h16 ":" ) ls32 + / "::" 5( h16 ":" ) ls32 + / [ h16 ] "::" 4( h16 ":" ) ls32 + / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + / [ *4( h16 ":" ) h16 ] "::" ls32 + / [ *5( h16 ":" ) h16 ] "::" h16 + / [ *6( h16 ":" ) h16 ] "::" + +h16 = 1*4HEXDIG +ls32 = ( h16 ":" h16 ) / IPv4address +IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet +dec-octet = DIGIT ; 0-9 + / %x31-39 DIGIT ; 10-99 + / "1" 2DIGIT ; 100-199 + / "2" %x30-34 DIGIT ; 200-249 + / "25" %x30-35 ; 250-255 + +reg-name = *( unreserved / pct-encoded / sub-delims ) + +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + +query = *( pchar / "/" / "?" ) + +fragment = *( pchar / "/" / "?" ) + +pct-encoded = "%" HEXDIG HEXDIG + +unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" +; end::rfc5234[] + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Base58 Bitcoin Alphabet +base58btc = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" \ No newline at end of file diff --git a/0004-general-uri-scheme/data/rad-uri.ne b/0004-general-uri-scheme/data/rad-uri.ne new file mode 100644 index 0000000..942c78c --- /dev/null +++ b/0004-general-uri-scheme/data/rad-uri.ne @@ -0,0 +1,245 @@ +@{% + const lstr = function(d) { return d.flat(5).join(''); } +%} + +# In the ABNF grammar, 'rad-resource-type-and-identity' defaults to empty, +# which effectively renders it optional. Nearley doesn't allow empty rules, +# and multiple rules with the same name can be added anyway, so we make +# resource_type_and_identity optional here to get the same essential semantics. +rad -> scheme ":" auth_and_resource resource_type_and_identity:? + ( "?" rad_query ):? ( "#" fragment ):? + {% ([s, , aar, rtai, q, f]) => + ({ scheme: s, + ...aar, + ...(rtai ? { resource: rtai} : {}), + ...(q ? { query: q[1]} : {}), + ...(f ? { fragment: f[1]} : {}) }) %} + | scheme ":" legacy + {% ([s, , l]) => ({ scheme: s, ...l }) %} + +scheme -> "rad" {% lstr %} + +auth_and_resource -> "//" auth:? "/" resource # absolute resource "path" + {% ([, auth = {}, , res]) => ({ ...auth, ...res }) %} + | resource # relative resource "path" + {% ([res]) => res %} + +# 'legacy' are URLs defined by RIP #3. That URL doesn't quite hold up +# against the general URI syntax defined in RFC3986, in so far that a node +# (identified with NID or NID@host) is the conceptual authority, not +# a repository. +# To be noted is that RIDs and NIDs are distinct enough that there shouldn't +# be any ambiguity between rad-auth-and-resource and rad-legacy. +# Of all current implementations, this mostly impacts the git remote helper +# git-remote-rad. +legacy -> "//" resource + {% ([, res]) => res %} + +# The NID is conceptually ambiguous from an RFC 3986 syntax perspective: +# +# authority = [ userinfo "@" ] host [ ":" port ] +# +# Is the NID 'userinfo' or 'host'? In a sense, it currently is a little bit +# of both. Furthermore, Radicle requires that if a hostname (+ port) is +# given, the NID must be given too. Therefore, instead of using 'authority' +# strictly as given by RFC 3986, we allow ourselves a slightly different +# syntax, which is fine. Do note also, that a 'host' can still be made to +# look like an NID, so programs dealing directly with the Radicle network +# will have to be prepared to handle that ambiguity appropriately. +# +# TODO: There already are ways to resolve its NID from a hostname. +# For example, if `radicle-httpd` runs next to the node, then it can be +# queried for its NID via HTTPS. Also, there are ways to use DNS TXT records, +# which is also done for DNS-SD. +# We would therefore likely make the `node` optional, or even omit it. +# If `node` is sufficiently different from a DNS name (it probably is +# not), then we can even omit "@". +auth -> node ("@" host_and_port):? + {% ([node, hap]) => ({ node: node, ...(hap ? { host: hap[1] } : {}) }) %} + +host_and_port -> host ":" port {% lstr %} + +# rad-node is a semantic symbol, to signify intent +node -> nid {% id %} + +# An NID is an ID that is ultimately a public key in the Radicle network. +# This is used to uniquely represent a node, or a user (also a node), or a +# namespace within a repository. +# It is a [multibase] and [multicodec] encoded string, using [base58btc] as the multibase +# and the codec for the underlying Ed25519 public key. +# +# It is always 48 characters long, including the initial "z6Mk", +# the `z` corresponding to the base, and `6Mk` corresponding to the codec. +# +# [base58btc]: https://digitalbazaar.github.io/base58-spec/ +# [multibase]: https://www.ietf.org/archive/id/draft-multiformats-multibase-07.html +# [multicodec]: https://github.com/multiformats/multicodec/ +# +# TODO: New NIDs in the future? Even this might change, as we allow rotating +# keys for nodes. But this is even further out than stable IDs for users. +nid -> "z6Mk" b58x25 b58x10 b58x5 base58btc base58btc base58btc base58btc {% lstr %} + +resource -> repository ( "/" namespace ):? + {% ([repo, ns]) => ({ repo: repo, ...(ns ? { namespace: ns[1] } : {}) }) %} + +# repository is a semantic symbol, to signify intent +repository -> rid {% id %} + +# namespace is a semantic symbol, to signify intent +namespace -> nid {% id %} + +# An RID uniquely represents a git repository in the Radicle network. +# It is a [multibase] encoded string of a Git OID, using [base58btc] as the multibase. +# +# It is 28 or 29 characters long, including the initial multibase identifier "z". +# +# [base58btc]: https://digitalbazaar.github.io/base58-spec/ +# [multibase]: https://www.ietf.org/archive/id/draft-multiformats-multibase-07.html +# +# TODO: In the near future (as soon as Git repositories with SHA-256 object +# format become popular, which will be in late 2026), we will have to extend +# the syntax of RIDs. Leave a note here so that implementors know this and +# make design decisions accordingly. +rid -> "z" b58x25 base58btc base58btc base58btc:? + {% (d,l,r) => { + s = d.join('') + // Check that the RID doesn't starts + // with "z6Mk", as that can only be + // an NID. Reject it if it is. + if (s.startsWith("z6Mk")) { return r; } + return s; + } + %} + +# For git specific resource types +resource_type_and_identity -> git_commit {% id %} +resource_type_and_identity -> git_tree {% id %} +resource_type_and_identity -> git_blob {% id %} +resource_type_and_identity -> git_tag {% id %} + +git_commit -> "/" "commit" "/" git_obj_or_ref {% ([, , , o]) => ({ type: "commit", ...o }) %} +git_tree -> "/" "tree" "/" git_obj {% ([, , , o]) => ({ type: "tree", obj: o }) %} +git_blob -> "/" "blob" "/" git_obj {% ([, , , o]) => ({ type: "blob", ref: o }) %} +git_tag -> "/" "tag" "/" git_obj_or_ref {% ([, , , o]) => ({ type: "tag", ...o }) %} + +# Note: this rule will always be ambiguous, as something that looks like a +# git object ID could very well be a branch or a tag. +# Some git commands will warn when such refs are used, but not all. +git_obj_or_ref -> git_obj {% ([o]) => ({ obj: o }) %} + | git_ref {% ([r]) => ({ ref: r }) %} + +# Currently, only sha1 identities are supported. +git_obj -> git_sha1 {% id %} +# a sha1 ID is 40 hex digits +git_sha1 -> hex8 hex8 hex8 hex8 hex8 {% lstr %} + +# unreserved is defined in RFC 3986. Not that this syntax allows much +# more than what can be specified for a git reference. It's left to +# the implementation to parse and apply semantics properly. +# Our best reference for how to interpret and parse git references is +# https://git-scm.com/docs/protocol-common.html +git_ref -> unreserved:+ ( "/" unreserved:+ ):* {% lstr %} + +# For Radicle collaborative object resource types +resource_type_and_identity -> rad_cob {% id %} +resource_type_and_identity -> rad_cobs {% id %} + +rad_cob -> "/" "cob" "/" rad_cob_type "/" rad_cob_obj + {% ([, , , type, , obj]) => + ({ type: "cob", id: { type: type, obj: obj } }) %} +rad_cobs -> "/" "cob" "/" rad_cob_type + {% ([, , , type]) => + ({ type: "cobs", id: { type: type } }) %} + +# The COB type is like a reversed FQDN. The syntax spec is derived from +# https://datatracker.ietf.org/doc/html/rfc1034#section-3.5 +rad_cob_type -> rad_cob_label ( "." rad_cob_label ):+ {% lstr %} +rad_cob_label -> alnum:+ ( "-" alnum:+ ):* {% lstr %} + +# syntactically, radicle COB objects and object revisions are git objects +# semantically, they are as well, at least for now +rad_cob_obj -> git_obj {% id %} + +# While the ABNF doesn't say anything special about 'query', we can allow +# it for ourselves here, since nearley does a bit more than just checking +# syntax. +rad_query -> rad_query_param ( "&" rad_query_param ):* + {% ([first, rest]) => { + newrest = rest ? rest : []; + newrest = newrest.reduce((tot, val) => { tot.push(val[1]); return tot }, []); + return ([first, ...newrest].reduce((tot, val) => { + if (tot[val['name']]) { + tot[val['name']].push(val['value']); + } else { + tot[val['name']] = [ val['value'] ]; + } + return tot; + }, {})) + } %} +rad_query_param -> ( alpha alnum:* ) "=" ( rad_query_pchar | "/" | "?" ):* + {% ([name, , value]) => ({ name: lstr(name), value: lstr(value) }) %} +rad_query_pchar -> unreserved | pct_encoded | rad_query_sub_delims | ":" | "@" +rad_query_sub_delims -> "!" | "$" | "'" | "(" | ")" + | "*" | "+" | "," | ";" | "=" + +# Helper symbols +alnum -> [a-zA-Z0-9] {% id %} +alnum5 -> alnum alnum alnum alnum alnum {% function(d) { return d.join(''); } %} +alnum10 -> alnum5 alnum5 {% function(d) { return d.join(''); } %} +alnum25 -> alnum10 alnum10 alnum5 {% function(d) { return d.join(''); } %} + +# Base58 Bitcoin Alphabet (excludes 0, O, I, l) +base58btc -> [1-9A-HJ-NP-Za-km-z] {% id %} +b58x5 -> base58btc base58btc base58btc base58btc base58btc {% function(d) { return d.join(''); } %} +b58x10 -> b58x5 b58x5 {% function(d) { return d.join(''); } %} +b58x25 -> b58x10 b58x10 b58x5 {% function(d) { return d.join(''); } %} + +hex2 -> hexdig hexdig {% function(d) { return d.join(''); } %} +hex4 -> hex2 hex2 {% function(d) { return d.join(''); } %} +hex8 -> hex4 hex4 {% function(d) { return d.join(''); } %} + +# ABNF defines ALPHA, DIGIT and HEXDIG +alpha -> [a-zA-Z] {% id %} +digit -> [0-9] {% id %} +hexdig -> [a-f0-9] {% id %} + +# RFC 3986 defines the following (translated to nearley) +host -> IP_literal | IPv4address | reg_name +port -> [0-9]:+ + +IP_literal -> "[" ( IPv6address | IPvFuture ) "]" + +IPvFuture -> "v" hexdig:+ "." ( unreserved | sub_delims | ":" ):+ + +IPv6address -> h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" ls32 + | "::" h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" ls32 + | h16 "::" h16 ":" h16 ":" h16 ":" h16 ":" ls32 + | h16 ":" h16 "::" h16 ":" h16 ":" h16 ":" ls32 + | h16 ":" h16 ":" h16 "::" h16 ":" h16 ":" ls32 + | h16 ":" h16 ":" h16 ":" h16 "::" h16 ":" ls32 + | h16 ":" h16 ":" h16 ":" h16 ":" h16 "::" ls32 + | h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" h16 "::" h16 + | h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" h16 "::" + +h16 -> hexdig | hex2 | hex2 hexdig | hex4 +ls32 -> ( h16 ":" h16 ) | IPv4address +IPv4address -> dec_octet "." dec_octet "." dec_octet "." dec_octet +dec_octet -> digit # 0-9 + | [1-9] digit # 10-99 + | "1" digit digit # 100-199 + | "2" [0-4] digit # 200-249 + | "25" [0-5] # 250-255 + +reg_name -> ( unreserved | pct_encoded | sub_delims ):* + +pchar -> unreserved | pct_encoded | sub_delims | ":" | "@" + +query -> ( pchar | "/" | "?" ):* + +fragment -> ( pchar | "/" | "?" ):* + +pct_encoded -> "%" hex2 + +unreserved -> alnum | "-" | "." | "_" | "~" +sub_delims -> "!" | "$" | "&" | "'" | "(" | ")" + | "*" | "+" | "," | ";" | "=" diff --git a/0004-general-uri-scheme/data/test/tc00a b/0004-general-uri-scheme/data/test/tc00a new file mode 100644 index 0000000..8a17444 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc00a @@ -0,0 +1 @@ +rad:z3gqcJUbbbbbbbbbbbbbbbCSGazv5 \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc00a-expected.json b/0004-general-uri-scheme/data/test/tc00a-expected.json new file mode 100644 index 0000000..e0581d6 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc00a-expected.json @@ -0,0 +1,6 @@ +[ + { + "scheme": "rad", + "repo": "z3gqcJUbbbbbbbbbbbbbbbCSGazv5" + } +] diff --git a/0004-general-uri-scheme/data/test/tc00b b/0004-general-uri-scheme/data/test/tc00b new file mode 100644 index 0000000..b189b6c --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc00b @@ -0,0 +1 @@ +rad://z3gqcJUbbbbbbbbbbbbbbbCSGazv5 \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc00b-expected.json b/0004-general-uri-scheme/data/test/tc00b-expected.json new file mode 100644 index 0000000..e0581d6 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc00b-expected.json @@ -0,0 +1,6 @@ +[ + { + "scheme": "rad", + "repo": "z3gqcJUbbbbbbbbbbbbbbbCSGazv5" + } +] diff --git a/0004-general-uri-scheme/data/test/tc00c b/0004-general-uri-scheme/data/test/tc00c new file mode 100644 index 0000000..a74bf68 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc00c @@ -0,0 +1 @@ +rad:///z3gqcJUbbbbbbbbbbbbbbbCSGazv5 \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc00c-expected.json b/0004-general-uri-scheme/data/test/tc00c-expected.json new file mode 100644 index 0000000..e0581d6 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc00c-expected.json @@ -0,0 +1,6 @@ +[ + { + "scheme": "rad", + "repo": "z3gqcJUbbbbbbbbbbbbbbbCSGazv5" + } +] diff --git a/0004-general-uri-scheme/data/test/tc01a b/0004-general-uri-scheme/data/test/tc01a new file mode 100644 index 0000000..cbacd81 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc01a @@ -0,0 +1 @@ +rad:zk1arLZbbbbbbbbbbbbbbb26us45E/z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc01a-expected.json b/0004-general-uri-scheme/data/test/tc01a-expected.json new file mode 100644 index 0000000..d9cee7e --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc01a-expected.json @@ -0,0 +1,7 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "namespace": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT" + } +] diff --git a/0004-general-uri-scheme/data/test/tc01b b/0004-general-uri-scheme/data/test/tc01b new file mode 100644 index 0000000..ecb7885 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc01b @@ -0,0 +1 @@ +rad://zk1arLZbbbbbbbbbbbbbbb26us45E/z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc01b-expected.json b/0004-general-uri-scheme/data/test/tc01b-expected.json new file mode 100644 index 0000000..d9cee7e --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc01b-expected.json @@ -0,0 +1,7 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "namespace": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT" + } +] diff --git a/0004-general-uri-scheme/data/test/tc01c b/0004-general-uri-scheme/data/test/tc01c new file mode 100644 index 0000000..b26b3d7 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc01c @@ -0,0 +1 @@ +rad:///zk1arLZbbbbbbbbbbbbbbb26us45E/z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc01c-expected.json b/0004-general-uri-scheme/data/test/tc01c-expected.json new file mode 100644 index 0000000..d9cee7e --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc01c-expected.json @@ -0,0 +1,7 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "namespace": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT" + } +] diff --git a/0004-general-uri-scheme/data/test/tc02 b/0004-general-uri-scheme/data/test/tc02 new file mode 100644 index 0000000..17aa320 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc02 @@ -0,0 +1 @@ +rad://z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT/zk1arLZbbbbbbbbbbbbbbb26us45E \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc02-expected.json b/0004-general-uri-scheme/data/test/tc02-expected.json new file mode 100644 index 0000000..e0b5bab --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc02-expected.json @@ -0,0 +1,7 @@ +[ + { + "scheme": "rad", + "node": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E" + } +] diff --git a/0004-general-uri-scheme/data/test/tc03 b/0004-general-uri-scheme/data/test/tc03 new file mode 100644 index 0000000..604b3e9 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc03 @@ -0,0 +1 @@ +rad://z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT/zk1arLZbbbbbbbbbbbbbbb26us45E/z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc03-expected.json b/0004-general-uri-scheme/data/test/tc03-expected.json new file mode 100644 index 0000000..38d1d63 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc03-expected.json @@ -0,0 +1,8 @@ +[ + { + "scheme": "rad", + "node": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "namespace": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT" + } +] diff --git a/0004-general-uri-scheme/data/test/tc04 b/0004-general-uri-scheme/data/test/tc04 new file mode 100644 index 0000000..ca38108 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc04 @@ -0,0 +1 @@ +rad://z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT@seed.example.com:8776/zk1arLZbbbbbbbbbbbbbbb26us45E \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc04-expected.json b/0004-general-uri-scheme/data/test/tc04-expected.json new file mode 100644 index 0000000..f1010be --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc04-expected.json @@ -0,0 +1,8 @@ +[ + { + "scheme": "rad", + "node": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT", + "host": "seed.example.com:8776", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E" + } +] diff --git a/0004-general-uri-scheme/data/test/tc05 b/0004-general-uri-scheme/data/test/tc05 new file mode 100644 index 0000000..1bbb703 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc05 @@ -0,0 +1 @@ +rad://z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT@seed.example.com:8776/zk1arLZbbbbbbbbbbbbbbb26us45E/z6Mku8bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbo9XVAx \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc05-expected.json b/0004-general-uri-scheme/data/test/tc05-expected.json new file mode 100644 index 0000000..b1e458a --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc05-expected.json @@ -0,0 +1,9 @@ +[ + { + "scheme": "rad", + "node": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT", + "host": "seed.example.com:8776", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "namespace": "z6Mku8bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbo9XVAx" + } +] diff --git a/0004-general-uri-scheme/data/test/tc10a b/0004-general-uri-scheme/data/test/tc10a new file mode 100644 index 0000000..191956f --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc10a @@ -0,0 +1 @@ +rad:z3gqcJUbbbbbbbbbbbbbbbCSGazv5/commit/72db6dffffffffffffffffffffffffffffffffff \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc10a-expected.json b/0004-general-uri-scheme/data/test/tc10a-expected.json new file mode 100644 index 0000000..0efe71b --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc10a-expected.json @@ -0,0 +1,18 @@ +[ + { + "scheme": "rad", + "repo": "z3gqcJUbbbbbbbbbbbbbbbCSGazv5", + "resource": { + "type": "commit", + "ref": "72db6dffffffffffffffffffffffffffffffffff" + } + }, + { + "scheme": "rad", + "repo": "z3gqcJUbbbbbbbbbbbbbbbCSGazv5", + "resource": { + "type": "commit", + "obj": "72db6dffffffffffffffffffffffffffffffffff" + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc10b b/0004-general-uri-scheme/data/test/tc10b new file mode 100644 index 0000000..7250dff --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc10b @@ -0,0 +1 @@ +rad:///z3gqcJUbbbbbbbbbbbbbbbCSGazv5/commit/72db6dffffffffffffffffffffffffffffffffff \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc10b-expected.json b/0004-general-uri-scheme/data/test/tc10b-expected.json new file mode 100644 index 0000000..0efe71b --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc10b-expected.json @@ -0,0 +1,18 @@ +[ + { + "scheme": "rad", + "repo": "z3gqcJUbbbbbbbbbbbbbbbCSGazv5", + "resource": { + "type": "commit", + "ref": "72db6dffffffffffffffffffffffffffffffffff" + } + }, + { + "scheme": "rad", + "repo": "z3gqcJUbbbbbbbbbbbbbbbCSGazv5", + "resource": { + "type": "commit", + "obj": "72db6dffffffffffffffffffffffffffffffffff" + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc12 b/0004-general-uri-scheme/data/test/tc12 new file mode 100644 index 0000000..7a63972 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc12 @@ -0,0 +1 @@ +rad://z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT/zk1arLZbbbbbbbbbbbbbbb26us45E/commit/e9cf5263ffffffffffffffffffffffffffffffff \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc12-expected.json b/0004-general-uri-scheme/data/test/tc12-expected.json new file mode 100644 index 0000000..1cb2519 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc12-expected.json @@ -0,0 +1,20 @@ +[ + { + "scheme": "rad", + "node": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "commit", + "ref": "e9cf5263ffffffffffffffffffffffffffffffff" + } + }, + { + "scheme": "rad", + "node": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "commit", + "obj": "e9cf5263ffffffffffffffffffffffffffffffff" + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc13 b/0004-general-uri-scheme/data/test/tc13 new file mode 100644 index 0000000..52d4903 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc13 @@ -0,0 +1 @@ +rad://z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT@seed.example.com:8776/zk1arLZbbbbbbbbbbbbbbb26us45E/z6Mku8bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbo9XVAx/commit/72db6dffffffffffffffffffffffffffffffffff \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc13-expected.json b/0004-general-uri-scheme/data/test/tc13-expected.json new file mode 100644 index 0000000..efa3f79 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc13-expected.json @@ -0,0 +1,24 @@ +[ + { + "scheme": "rad", + "node": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT", + "host": "seed.example.com:8776", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "namespace": "z6Mku8bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbo9XVAx", + "resource": { + "type": "commit", + "ref": "72db6dffffffffffffffffffffffffffffffffff" + } + }, + { + "scheme": "rad", + "node": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT", + "host": "seed.example.com:8776", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "namespace": "z6Mku8bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbo9XVAx", + "resource": { + "type": "commit", + "obj": "72db6dffffffffffffffffffffffffffffffffff" + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc30 b/0004-general-uri-scheme/data/test/tc30 new file mode 100644 index 0000000..9deebf7 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc30 @@ -0,0 +1 @@ +rad:zk1arLZbbbbbbbbbbbbbbb26us45E/cob/org.example/e9cf5263ffffffffffffffffffffffffffffffff \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc30-expected.json b/0004-general-uri-scheme/data/test/tc30-expected.json new file mode 100644 index 0000000..e5114d6 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc30-expected.json @@ -0,0 +1,13 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "cob", + "id": { + "type": "org.example", + "obj": "e9cf5263ffffffffffffffffffffffffffffffff" + } + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc31 b/0004-general-uri-scheme/data/test/tc31 new file mode 100644 index 0000000..ab6fe8c --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc31 @@ -0,0 +1 @@ +rad://z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT/zk1arLZbbbbbbbbbbbbbbb26us45E/cob/org.example/e9cf5263ffffffffffffffffffffffffffffffff \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc31-expected.json b/0004-general-uri-scheme/data/test/tc31-expected.json new file mode 100644 index 0000000..9348c48 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc31-expected.json @@ -0,0 +1,14 @@ +[ + { + "scheme": "rad", + "node": "z6MksFbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbS9wzpT", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "cob", + "id": { + "type": "org.example", + "obj": "e9cf5263ffffffffffffffffffffffffffffffff" + } + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc32 b/0004-general-uri-scheme/data/test/tc32 new file mode 100644 index 0000000..89c768e --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc32 @@ -0,0 +1 @@ +rad:zk1arLZbbbbbbbbbbbbbbb26us45E/cob/org.example diff --git a/0004-general-uri-scheme/data/test/tc32-expected.json b/0004-general-uri-scheme/data/test/tc32-expected.json new file mode 100644 index 0000000..4d88c69 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc32-expected.json @@ -0,0 +1,12 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "cobs", + "id": { + "type": "org.example" + } + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc40a b/0004-general-uri-scheme/data/test/tc40a new file mode 100644 index 0000000..1184f53 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc40a @@ -0,0 +1 @@ +rad:zk1arLZbbbbbbbbbbbbbbb26us45E/tree/3eb47e9fffffffffffffffffffffffffffffffff?path=src \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc40a-expected.json b/0004-general-uri-scheme/data/test/tc40a-expected.json new file mode 100644 index 0000000..70c8b45 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc40a-expected.json @@ -0,0 +1,15 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "tree", + "obj": "3eb47e9fffffffffffffffffffffffffffffffff" + }, + "query": { + "path": [ + "src" + ] + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc40b b/0004-general-uri-scheme/data/test/tc40b new file mode 100644 index 0000000..d04f609 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc40b @@ -0,0 +1 @@ +rad:///zk1arLZbbbbbbbbbbbbbbb26us45E/tree/3eb47e9fffffffffffffffffffffffffffffffff?path=src \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc40b-expected.json b/0004-general-uri-scheme/data/test/tc40b-expected.json new file mode 100644 index 0000000..70c8b45 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc40b-expected.json @@ -0,0 +1,15 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "tree", + "obj": "3eb47e9fffffffffffffffffffffffffffffffff" + }, + "query": { + "path": [ + "src" + ] + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc41a b/0004-general-uri-scheme/data/test/tc41a new file mode 100644 index 0000000..c0c60c8 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc41a @@ -0,0 +1 @@ +rad:zk1arLZbbbbbbbbbbbbbbb26us45E/commit/master?blob=README.md \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc41a-expected.json b/0004-general-uri-scheme/data/test/tc41a-expected.json new file mode 100644 index 0000000..518a65e --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc41a-expected.json @@ -0,0 +1,15 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "commit", + "ref": "master" + }, + "query": { + "blob": [ + "README.md" + ] + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc41b b/0004-general-uri-scheme/data/test/tc41b new file mode 100644 index 0000000..baaaa9e --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc41b @@ -0,0 +1 @@ +rad:///zk1arLZbbbbbbbbbbbbbbb26us45E/commit/master?blob=README.md \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc41b-expected.json b/0004-general-uri-scheme/data/test/tc41b-expected.json new file mode 100644 index 0000000..518a65e --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc41b-expected.json @@ -0,0 +1,15 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "commit", + "ref": "master" + }, + "query": { + "blob": [ + "README.md" + ] + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc42a b/0004-general-uri-scheme/data/test/tc42a new file mode 100644 index 0000000..122e558 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc42a @@ -0,0 +1 @@ +rad:zk1arLZbbbbbbbbbbbbbbb26us45E/commit/baz?path=foo/doc \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc42a-expected.json b/0004-general-uri-scheme/data/test/tc42a-expected.json new file mode 100644 index 0000000..da01aa9 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc42a-expected.json @@ -0,0 +1,15 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "commit", + "ref": "baz" + }, + "query": { + "path": [ + "foo/doc" + ] + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc42b b/0004-general-uri-scheme/data/test/tc42b new file mode 100644 index 0000000..c5d75ee --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc42b @@ -0,0 +1 @@ +rad:///zk1arLZbbbbbbbbbbbbbbb26us45E/commit/baz?path=foo/doc \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc42b-expected.json b/0004-general-uri-scheme/data/test/tc42b-expected.json new file mode 100644 index 0000000..da01aa9 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc42b-expected.json @@ -0,0 +1,15 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "commit", + "ref": "baz" + }, + "query": { + "path": [ + "foo/doc" + ] + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc43a b/0004-general-uri-scheme/data/test/tc43a new file mode 100644 index 0000000..2adf2e6 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc43a @@ -0,0 +1 @@ +rad:zk1arLZbbbbbbbbbbbbbbb26us45E/commit/baz/foo?path=doc&path=src \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc43a-expected.json b/0004-general-uri-scheme/data/test/tc43a-expected.json new file mode 100644 index 0000000..3e4304e --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc43a-expected.json @@ -0,0 +1,16 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "commit", + "ref": "baz/foo" + }, + "query": { + "path": [ + "doc", + "src" + ] + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc43b b/0004-general-uri-scheme/data/test/tc43b new file mode 100644 index 0000000..04eceb3 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc43b @@ -0,0 +1 @@ +rad:///zk1arLZbbbbbbbbbbbbbbb26us45E/commit/baz/foo?path=doc&path=src \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc43b-expected.json b/0004-general-uri-scheme/data/test/tc43b-expected.json new file mode 100644 index 0000000..3e4304e --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc43b-expected.json @@ -0,0 +1,16 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "commit", + "ref": "baz/foo" + }, + "query": { + "path": [ + "doc", + "src" + ] + } + } +] diff --git a/0004-general-uri-scheme/data/test/tc44 b/0004-general-uri-scheme/data/test/tc44 new file mode 100644 index 0000000..47cb243 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc44 @@ -0,0 +1 @@ +rad:///zk1arLZbbbbbbbbbbbbbbb26us45E/commit/refs/notes/commit?blob=e9cf5263ffffffffffffffffffffffffffffffff \ No newline at end of file diff --git a/0004-general-uri-scheme/data/test/tc44-expected.json b/0004-general-uri-scheme/data/test/tc44-expected.json new file mode 100644 index 0000000..298e625 --- /dev/null +++ b/0004-general-uri-scheme/data/test/tc44-expected.json @@ -0,0 +1,15 @@ +[ + { + "scheme": "rad", + "repo": "zk1arLZbbbbbbbbbbbbbbb26us45E", + "resource": { + "type": "commit", + "ref": "refs/notes/commit" + }, + "query": { + "blob": [ + "e9cf5263ffffffffffffffffffffffffffffffff" + ] + } + } +] diff --git a/0004-general-uri-scheme/data/test/uri-scheme-tester.sh b/0004-general-uri-scheme/data/test/uri-scheme-tester.sh new file mode 100755 index 0000000..5616044 --- /dev/null +++ b/0004-general-uri-scheme/data/test/uri-scheme-tester.sh @@ -0,0 +1,42 @@ +#! /bin/sh + +set -euo pipefail + +if ! command -v nearley-test > /dev/null; then + echo >&2 'The command `nearley-test` must be available to run this script. Consider running `npm install -g nearley`.' + exit 2 +fi + +if ! command -v yq > /dev/null; then + echo >&2 'The command `yq` must be available to run this script.' + exit 2 +fi + +readonly DATA="${1:-"$(git rev-parse --show-toplevel)/0004-general-uri-scheme/data"}" + +readonly RAD_URI_NE="${DATA}/rad-uri.ne" +readonly RAD_URI_JS="$TMPDIR/rad-uri.js" + +if ! nearleyc -o "$RAD_URI_JS" "$RAD_URI_NE"; then + exit 1 +fi + +readonly TMP="$(mktemp -d)" + +FAILED=0 +for EXPECTED in "${DATA}"/test/tc*-expected.json; do + INPUT="${EXPECTED%-expected.json}" + NAME="${INPUT##*/}" + ACTUAL="${TMP}/${NAME}.actual" + + nearley-test -q -i "$(cat "$INPUT")" "$RAD_URI_JS" | yq > "${ACTUAL}" + + if ! diff -u "$EXPECTED" "$ACTUAL" ; then + echo "FAIL: $NAME" + FAILED=1 + else + echo "PASS: $NAME" + fi +done + +[ "$FAILED" -eq 0 ] diff --git a/0004-general-uri-scheme/general-uri-scheme.adoc b/0004-general-uri-scheme/general-uri-scheme.adoc new file mode 100644 index 0000000..d4773b5 --- /dev/null +++ b/0004-general-uri-scheme/general-uri-scheme.adoc @@ -0,0 +1,867 @@ += RIP 4: General `rad:` URI Scheme +Lorenz Leutgeb ; Richard Levitte +:doctype: article +:toc: preamble +:toclevels: 3 +:status: Draft +:copyright: CC0-1.0 +:lang: en + +-- +In <>, a `rad:` URN is briefly discussed as a simple Radicle +identifier. In <>, the `rad://` URL is introduced, made to allow +`git-remote-rad` to be a fruitful interface between Git and Radicle. + +In this RIP, we discuss some modifications that are needed to have a general +URI scheme that fits in the syntax defined by <>, as well as further +extensions that allow reference to more aspects of Radicle as well as Git on a +Radicle network. + +All extensions and modifications are intended not to introduce impossible +clashes with the existing schemes. +-- + +== Status Quo + +References to resources on Radicle (both traditional Git objects as well as +collaborative objects, such as patches and issues) are communicated between +individuals mostly by relying on installations of <>, +a web application that provides an HTTPS interface to the Radicle network. + +When Alice wants to direct Bob's attention to a specific issue that she filed +on Radicle, you will likely see her send a URI of the form +`https://app.example.com/nodes/seed.example.com/rad:…/issues/…` to Bob. +URIs using the `rad:` schema are exchanged more rarely, and almost exclusively +to refer to repositories as a whole, e.g. in order to invoke `rad clone`, +`git clone`, etc. + +As of 2024-04-23, the Radicle team also opted to have `rad:…` +URIs in Zulip messages automatically replaced by links to +`https://app.radicle.xyz/nodes/garden.radicle.xyz/rad:…`, limiting the +usefulness of exchange of `rad:` URIs.<> + +== Terminology + +Node ID:: a Node ID, or NID for short, is an identifier for a Radicle node and a Radicle user, at the time of writing. +It is a <> (<>) and <> encoded Ed25519 public key. + +Namespace:: a namespace, in the Radicle ecosystem, commonly refers to a Git namespace, i.e. `refs/namespaces`, +that uses a *Node ID* as the namespace, e.g., `refs/namespaces/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi`. + +Repository ID:: a Repository ID, or RID for short, is an identifier for a Radicle repository. +It is a <> (<>) encoded Git object identifier, at the time of writing. + +== Challenges + +:url-cgit: https://git.zx2c4.com/cgit/ +:url-sourcegraph: https://sourcegraph.com/ + +Every user should be able to individually control how they want to interact +with the network. Users may prefer to interact with Radicle via… + +* a command line interface in their shell. +* a specific client program, such as: +** VSCode with a suitable extension, <> +** a JetBrains IDE with a suitable plugin, <>. +* <>, hosted locally or by relying on a specific hosting + provider. +* another custom program or handler, e.g. redirection to an instance of + {url-cgit}[cgit] or {url-sourcegraph}[Sourcegraph]. + +Exchanging `https:` URIs that point at a specific hosting provider of +<> is not portable. Individuals that read/receive such a URI +might prefer to access the repository with another method and then must modify +the URI (extract repository address, etc.) before access. + +For the case of a reference to a repository, this is rather simple, but as +soon as references to resources _within_ the repositories are exchanged, there +are currently no standardized options. The two most common ones are referring +to an instance of `radicle-explorer` by means of a `https:`-URI, or +communicating in natural language, often in a context where the repository ID +is clear to all communicating parties. They will simply say "patch c863fe". + +Automatic rewriting of `rad:` URIs to a specific hosting provider of +`radicle-explorer` contributes to additional centralization of access to the +network. In case the specific hosting provider goes offline, is censored, or +censors content, the impact is multiplied. + +== Intention + +The intention with this RIP is to define a `rad:` URI scheme with which one +can refer to: + +. a Git repository in the Radicle network, which also includes the namespace + for the contributions of a particular user (currently represented with a NID). +. optionally, a repository resource of some sort; to properly identify + the resource: +.. the resource's type +.. a resource identity, specific to the resource type + +**** +We recognise that the concept of namespace and how that relates to a user is +a bit ambiguous. A more detailed explanation should appear in another RIP. + +For the moment, the user and their local node are identified with the same +identity, the NID. Since the namespace for a user uses its identity, it may +appear confusing. However, the fact remains that a namespace stores a user's +contribution to the repository it lives within. + +In the future, users may be identified differently, and when that happens, the +namespace is expected to change accordingly, so that it is clear that it is the +user's contributions. +**** + +This can be seen as a hierarchy, expressed like this: + + repo-id [ "/" namespace ] [ "/" resource-type "/" resource-id ] + +The `rad:` URI scheme is intended to be used to refer to / represent +repositories and resources within repositories, in such a way that it +can be used by applications, but also be understood by humans wishing +to get to the resource being referred to with what knowledge tools +they have available (such as the commands `git` and `rad`). + +It does not imply any particular network protocol, or even that all +the parts of the scheme must be implemeted by all applications, and +applications may also make some parts of the scheme more mandatory +than what this RIP implies. + +As a very concrete example of an application that handles `rad:` URIs, +`git-remote-rad` can probably never be expected to handle more than a +repository and namespace, a `rad:` URI with more than that should be +considered an error. On the other hand, `git-remote-rad` requires the +presence of a namespace, i.e. makes it mandatory. + +== Formalization and General Extension of the `rad:` URI Scheme + +In this RIP, we denote the `rad:` scheme as a _URI_ scheme and try to +generally follow the nomenclature from <>. + +=== The `web+rad:` URI Scheme + +<> defines that unless a URL scheme is known and on their safelist, it +may be registered as a custom URL handler if prefixed with `web+`. See their +section on <>. + +For this RIP, it means that `web+rad:` must be compatible with `rad:` in every +way, i.e prepending the string `"web+"` to a valid `rad:` URI must result in a +valid `web+rad:`-URI, and removing the first four symbols from a `web+rad:` +URI must result in a valid `rad:` URI. + +This should make it as easy as possible for users to register URI handlers for +both `web+rad:` and `rad:` on their system, according to their preferences. + +=== The Old `rad:` URI Scheme + +The scheme as defined in <> and <> was really pretty simple, and could +be summarized with just a few lines of <>: + +// [source,abnf] +[%nowrap] +---- +rad = rad-scheme ":" rad-rid ; (RIP #2) + / rad-scheme ":" "//" rad-rid [ "/" rad-nid ] ; (RIP #3) +rad-scheme = "rad" +rad-rid = "z" 27*28(ALPHA / DIGIT) +rad-nid = "z6Mk" 44(ALPHA / DIGIT) +---- + +This is workable in practice, but the URL form is arguably not aligning with +the ideas behind the 'authority' as described in <>. The +'authority' carries with it the idea of a host, possibly with some user +information. In a Radicle network, it can be argued that the NID represents +that much more than the RID does, but the NID used in the old `rad:` URI +scheme does not represent that either; it represents a namespace within a +repository. + +=== The New `rad:` URI Scheme: General Approach + +To align with <>, the `rad:` scheme is renewed to include a +possible authority, which is the particular node / host that the data should +be found on. In most cases, (much like in `file:` URIs), the authority is +expected to be empty or not there at all. + +Other than that, the new URI scheme is extended with additional syntax to +meet the <> given above. + +// [source,abnf] +[%nowrap] +---- +include::data/rad-uri.abnf[tag=overview] +---- + +==== The Authority + +The NID is conceptually ambiguous from an <> syntax perspective: + +// [source,abnf] +[%nowrap] +---- +authority = [ userinfo "@" ] host [ ":" port ] +---- + +This does not quite align semantically with how Radicle nodes are identified. +Radicle nodes are identified with their NID, or with `{NID}@{hostname}:{port}`. +This does align with <> syntax well enough, with just a little +bit of redefinition of the authority for use in Radicle context. + +**** +The authority is most closely related to the address given to the Radicle CLI +command `rad node connect`. +**** + +// [source,abnf] +[%nowrap] +---- +include::data/rad-uri.abnf[tag=authority] +---- + +==== The Repository + +After the authority (if there is one) comes the repository to refer to. + +With Radicle, the main anchoring resource is the repository, so its ID must +always be present, and specifying it alone refers to the synthesis of all +participants' contributions. + +To refer to individual participants' separate contribution, their namespace +must also be included in the URI, separate from the repository ID with a slash +("/"). It shall be noted, though, that the namespace is only meaningful in +some specific circumstances, to be defined later. + +**** +Radicle repositories are _currently_ Git repositories. We expect that other +version control (VCS) implementations may be used as well, and in that case, +we expect the VCS implementation type to be reflected in the repository +identity. +**** + +// [source,abnf] +[%nowrap] +---- +include::data/rad-uri.abnf[tag=resource] +---- + +Here are a few examples of what a reference to a whole repository could look +like: + +:tc00a: pass:c,q[footnote:tc00a[nearley-test result in link:data/test/tc00a-expected.json[]]] +:tc00b: pass:c,q[footnote:tc00b[nearley-test result in link:data/test/tc00b-expected.json[]]] +:tc00c: pass:c,q[footnote:tc00c[nearley-test result in link:data/test/tc00c-expected.json[]]] +:tc01a: pass:c,q[footnote:tc01a[nearley-test result in link:data/test/tc01a-expected.json[]]] +:tc01b: pass:c,q[footnote:tc01b[nearley-test result in link:data/test/tc01b-expected.json[]]] +:tc01c: pass:c,q[footnote:tc01c[nearley-test result in link:data/test/tc01c-expected.json[]]] +:tc02: pass:c,q[footnote:tc02[nearley-test result in link:data/test/tc02-expected.json[]]] +:tc03: pass:c,q[footnote:tc03[nearley-test result in link:data/test/tc03-expected.json[]]] +:tc04: pass:c,q[footnote:tc04[nearley-test result in link:data/test/tc04-expected.json[]]] +:tc05: pass:c,q[footnote:tc05[nearley-test result in link:data/test/tc05-expected.json[]]] + +[options="header",cols="2a,1"] +|=== +|URI +|Refers to + +| +.... +include::data/test/tc00a[] +.... +{tc00a} +|Specific repo (RIP #2 URN form, still valid) + +| +.... +include::data/test/tc00b[] +.... +{tc00b} +|Specific repo (RIP #3 URL form, considered legacy) + +| +.... +include::data/test/tc00c[] +.... +{tc00c} +|Specific repo (new URI form with empty authority) + +| +.... +include::data/test/tc01a[] +.... +{tc01a} +|Specific repo and namespace (new URI form, relative) + +| +.... +include::data/test/tc01b[] +.... +{tc01b} +|Specific repo and namespace (RIP #3 URL form, considered legacy) + +| +.... +include::data/test/tc01c[] +.... +{tc01c} +|Specific repo and namespace (new URI form with empty authority) + +| +.... +include::data/test/tc02[] +.... +{tc02} +|Specific repo, which, if not available locally, should be fetched from the node `z6MksF…S9wzpT` + +| +.... +include::data/test/tc03[] +.... +{tc03} +|Specific repo and namespace, which, if not available locally, should be fetched from the node `z6MksF…S9wzpT` + +| +.... +include::data/test/tc04[] +.... +{tc04} +|Specific repo, which, if not available locally, should be fetched from the node `z6MksF…S9wzpT`, available on the host `seed.example.com:8776` + +| +.... +include::data/test/tc05[] +.... +{tc05} +|Specific repo and namespace, which, if not available locally, should be fetched from the node `z6MksF…S9wzpT`, available on the host `seed.example.com:8776` +|=== + +(to test these URIs against a grammar, see <> below) + +==== The Resource Type and Identity + +While <> allows you to refer to a Git repository, +it may also be desirable to refer to one or more specific resources within +that repository. To do this, <> may be followed +by a "/", a resource type and identity. + +By default, the resource type and identity is nothing, left to be added +to further on in this RIP, or in future RIPs: + +// [source,abnf] +[%nowrap] +---- +include::data/rad-uri.abnf[tags=resource-type-and-identity;!*] +---- + +==== The Query + +The query part of the `rad:` URI inherits the syntax from <>. +The semantics of the query are to be defined with each resource type. + +==== The Fragment + +The fragment part of the URI inherits the syntax and semantics from <>. + +=== Relative URIs + +<> discusses the resolution of relative (absolute +or rootless alike) URIs, based on a base URI. + +This RIP has nothing specific to say about this, apart from supporting it. + +== Resource Types and Identities + +These are the resource types and associated identities defined in this +RIP. Future RIPs may amend them, as well as define new resource types +with associated identities. + +=== Resource Types Specific to Git + +There are four Git specific resource types: + +* `commit` +* `tree` +* `blob` +* `tag` + +In a `rad:` URI, these resource types are used to refer to Git object types +with the same name in a repository. These resource types must be followed by +an identifier that refers to a specific Git object of that type. + +Some of these resource types may also be accompanied by <> +parameters to further refine what the `rad:` URI refers to. This RIP defines +the following <> parameters, all of which may be given +multiple times: + +* `tree=dir1/dir2/…`, to "dig out" a tree given by the path. +* `blob=dir1/dir2/…`, to "dig out" a blob given by the path. +* `path=dir1/dir2/…`, to "dig out" a tree or a blob given by the path. + +**** +Applications are by and large allowed to define other <> +parameters than what is defined here. We hope that future RIPs will help +standardise at least the most common ones. + +A few possible examples that may be useful: + +* `rad:{rid}/commit/{git-obj-or-ref}?history`, to view the history of a commit. +* `rad:{rid}/blob/{git-obj}?raw`, to view/download the raw contents of a blob, + where one might otherwise reasonably expected it to be wrapped in some + markup, such as HTML. +**** + +`commit`:: ++ +-- +The identifier that follows this resource type may be: + +* a Git object ID +* a Git reference (typically, a branch or a tag name) + +In all cases, this resource type refers to a single commit object. + +With this resource type, it makes sense to use the <> +parameters `tree`, `blob` and `path` to refine the `rad:` URI. +-- + +`tree`:: ++ +-- +The identifier that follows this type may be must be a Git object identifier. + +With this resource type, it makes sense to use the <> +parameters `tree`, `blob` and `path` to refine the `rad:` URI. +-- + +`blob`:: ++ +-- +The identifier that follows this type must be a Git object identifier. + +With this resource type, it makes no sense to use any of the <> defined above. +-- + +`tag`:: ++ +-- +The identifier that follows this type may be: + +* a Git object ID +* a Git reference (i.e. a tag name) + +In all cases, this resource type refers to a single tag object. + +With this resource type, it makes sense to use the <> +parameters `tree`, `blob` and `path` to refine the `rad:` URI. +-- + +In <> terms, the Git specific resource types are defined like this: + +// [source,abnf] +[%nowrap] +---- +include::data/rad-uri.abnf[tag=git-type-and-identity] +---- + +Here are a few examples of what a reference to a Git specific resource could +look like: + +:tc10a: pass:c,q[footnote:tc10a[nearley-test result in link:data/test/tc10a-expected.json[]]] +:tc10b: pass:c,q[footnote:tc10b[nearley-test result in link:data/test/tc10b-expected.json[]]] +:tc12: pass:c,q[footnote:tc12[nearley-test result in link:data/test/tc12-expected.json[]]] +:tc13: pass:c,q[footnote:tc13[nearley-test result in link:data/test/tc13-expected.json[]]] + +[options="header",cols="2a,1"] +|=== +|URI +|Refers to + +| +.... +include::data/test/tc10a[] +.... +{tc10a} +|Specific commit in repo + +| +.... +include::data/test/tc10b[] +.... +{tc10b} +|Specific commit in repo + +| +.... +include::data/test/tc12[] +.... +{tc12} +|Specific commit on a specific node, using the explicit name for the commit object + +| +.... +include::data/test/tc13[] +.... +{tc13} +|Specific commit in a specific repo and namespace, which, if not available locally, should be fetched from the node `z6MksF…S9wzpT`, available on the host `seed.example.com:8776` +|=== + +(to test these URIs against a grammar, see <> below) + +And with queries: + +:tc40a: pass:c,q[footnote:tc40a[nearley-test result in link:data/test/tc40a-expected.json[]]] +:tc40b: pass:c,q[footnote:tc40b[nearley-test result in link:data/test/tc40b-expected.json[]]] +:tc41a: pass:c,q[footnote:tc41a[nearley-test result in link:data/test/tc41a-expected.json[]]] +:tc41b: pass:c,q[footnote:tc41b[nearley-test result in link:data/test/tc41b-expected.json[]]] +:tc42a: pass:c,q[footnote:tc42a[nearley-test result in link:data/test/tc42a-expected.json[]]] +:tc42b: pass:c,q[footnote:tc42b[nearley-test result in link:data/test/tc42b-expected.json[]]] +:tc43a: pass:c,q[footnote:tc43a[nearley-test result in link:data/test/tc43a-expected.json[]]] +:tc43b: pass:c,q[footnote:tc43b[nearley-test result in link:data/test/tc43b-expected.json[]]] +:tc44: pass:c,q[footnote:tc44[nearley-test result in link:data/test/tc44-expected.json[]]] + +[options="header",cols="2a,1"] +|=== +|URI +|Refers to + +| +.... +include::data/test/tc40a[] +.... +{tc40a} +|The `src` directory of the repo at tree object `3eb47e9` + +| +.... +include::data/test/tc40b[] +.... +{tc40b} +|The `src` directory of the repo at tree object `3eb47e9` + +| +.... +include::data/test/tc41a[] +.... +{tc41a} +|The `README.md` file of the repo at the "master" reference + +| +.... +include::data/test/tc41b[] +.... +{tc41b} +|The `README.md` file of the repo at the "master" reference + +| +.... +include::data/test/tc42a[] +.... +{tc42a} +|The `foo/doc` directory of the repo at the "baz" reference + +| +.... +include::data/test/tc42b[] +.... +{tc42b} +|The `foo/doc` directory of the repo at the "baz" reference + +| +.... +include::data/test/tc43a[] +.... +{tc43a} +|The `doc` and `src` directories of the repo at the "baz/foo" reference. + +| +.... +include::data/test/tc43b[] +.... +{tc43b} +|The `doc` and `src` directories directory of the repo at the "baz/foo" reference. + +| +.... +include::data/test/tc44[] +.... +{tc44} +|The commit notes for commit `e9cf5263…`. +|=== + +(to test these URIs against a grammar, see <> below) + +**** +// Git version that is current at the time of writing. +:gitversion: 2.54.0 + +// Base URL of Git documentation. +:gitdocs: https://git-scm.com/docs + +// Allows for convenient reference to definitions in the Git glossary. +// "gitgloss" is just short for "a definition in the Git glossary". +:gitgloss: {gitdocs}/gitglossary/{gitversion}#Documentation/gitglossary.txt- + +:gitrevparse: {gitdocs}/git-rev-parse/{gitversion}#Documentation/git-rev-parse.txt- + +[discrete] +==== Addressing Git Resources by Reference Names + +[discrete] +===== Types of Resources That May Be Addressed + +Resources of the types "commit" and "tag", may, in Git jargon, be +"committish". There are at least two definitions in the Git documentation +that differ slightly: + +[quote,'{gitgloss}commit-ishalsocommittish[Git glossary entry for "committish"]'] +A {gitgloss}commit_object[commit object] or an {gitgloss}object[object] +that can be recursively {gitgloss}dereference[dereferenced] to a commit +object. The following are all commit-ishes: +a commit object, +a {gitgloss}tag_object[tag object] that points to a commit object, +a tag object that points to a tag object that points to a commit object, +etc. + +[quote,'{gitrevparse}--verify[Git documentation of `git rev-parse --verify`]'] +a commit-ish (i.e. a commit, or an annotated tag that points at a commit) + +We thus restrict ourselves to only refer to commits and tags via references, +since no tree or blob object may ever be committish. + +[discrete] +===== Ambiguity in Addressing by Reference Name + +Note that Git references are ambiguous, even within one repository (or reference +namespace). + +It is possible to create the following set of references in one namespace: + +* `refs/heads/main` +* `refs/tags/main` +* `refs/heads/refs/heads/main` + +We do not define how such ambiguity is to be resolved, but note that applications +will likely want to implement some mechanism for disambiguation. + +See also {gitrevparse}refnameegmasterheadsmasterrefsheadsmaster[the section +_Specifying Revisions_ in the documentation of `git rev-parse`]. + +**** + +=== Resources Types Specific to Radicle Collaborative Objects + +Alongside the usual Git objects, Radicle also implements a generalized +collaborative object (usually referred to as COB), identified with a COB type +and a COB object. + +For general COB references in a `rad:` URI, these resource types are defined +to refer to a Radicle COB / multiple COBs: + +`cob`:: ++ +-- +In a `rad:` URI, this must be followed a slash (`/`) and the COB type. The +COB type _may_ be followed by another slash (`/`) and the COB object. + +If the `rad:` URI includes the COB object, then it refers to a single COB. + +If the `rad:` URI does not include the COB object, then it refers to the set +of COB objects of the given COB type. +-- + +`cob` resources may also be accompanied by a set of <> +parameters to further refine what the `rad:` URI refers to. + +**** +Because we realise that we cannot know all future COB types and how they +function, we cannot do too much in terms of definitions. +We leave it to the COB owners as well as applications to work out what query params they +will handle, and hopefully, some of them can be standardised in future RIPs. + +A hypothetical example: `rad:{RID}/cob/example.radicle.sparkle?q=glitter`. +**** + +In <> terms, the COB resource types are defined like this: + +// [source, abnf] +[%nowrap] +---- +include::data/rad-uri.abnf[tag=rad-cob-type-and-identity] +---- + +Here are a few examples of what a reference to other collaborative objects +could look like: + +:tc30: pass:c,q[footnote:tc30[nearley-test result in link:data/test/tc30-expected.json[]]] +:tc31: pass:c,q[footnote:tc31[nearley-test result in link:data/test/tc31-expected.json[]]] +:tc32: pass:c,q[footnote:tc32[nearley-test result in link:data/test/tc32-expected.json[]]] + +[options="header",cols="2a,1"] +|=== +|URI +|Refers to + +| +.... +include::data/test/tc30[] +.... +{tc30} +|Specific COB of the type `org.example` + +| +.... +include::data/test/tc31[] +.... +{tc31} +|Specific COB of the type `org.example` on specific node + +| +.... +include::data/test/tc32[] +.... +{tc32} +|The set of COBs of the type `org.example` +|=== + +== Implementation Requirements and Recommendations + +* Implementations may implement only part of this specification, depending on +what makes sense in the context of what they do. It is advisable, however, if +they can parse enough of a valid `rad:` URI to allow for <>. +For example, it is hard to imagine that `git-remote-rad` would ever support +more elaborate URLs than those involving an RID and an NID: + +** `rad:{RID}` +** `rad:///{RID}` +** `rad:{RID}/{NID}` +** `rad:///{RID}/{NID}` +** `rad://{RID}/{NID}` (legacy) + +* Implementations that receive a URI they cannot handle - because of +unrecognised resource type, identifier, or query params - should signal an +error. ++ +For fragments, there is no recommendation, that is left entirely to the +authors of those softwares. + +* Software for human interaction on the web should be able to automatically +convert between `rad:` and `web+rad:` to create links. For example, in a +markdown context, it would be quite viable for this: ++ +`rad:z3gqcJU…CSGazv5/commit/72db6d…` ++ +to be converted to: ++ +`[rad:z3gqcJU…CSGazv5/commit/72db6d…](web+rad:z3gqcJU…CSGazv5/commit/72db6d…)` + +* Implementations that are aware of the context may support relative URIs as +they see fit. For example, a program that handles Radicle patches and issues +could assume the base URI `rad:///{RID}` (given that `{RID}` is the ID of +the repository that is being handled), allowing comments to refer to issue and +other patches in the same repository with `patch/{PATCH-ID}` or `issue/{ISSUE-ID}`. + +== Future Work + +Future work may include: + +* amending <> with Radicle network + specification syntax (pending future formalization of that idea). +* amending <> with multi-device IDs (also + pending formalization of that idea). +* adding more resource types, and their respective syntaxes +* extending existing resource types. +* adding other query types +* viewing the history/graph of the underlying Git repository (in analogy to `git log`/`git log --graph`). +* viewing revision ranges of the underlying Git repository (in analogy to `git diff`, `git log`). + +== Closing Thoughts + +This RIP started with a status quo and challenges. It is the hope of the +authors that the challenges are sufficiently addressed, while also leaving +doors open for more extensions in the future. + +== Copyright + +This document is licensed under the Creative Commons CC0 1.0 Universal license. + +[bibliography] +== References + +// https://docs.asciidoctor.org/asciidoc/latest/syntax-quick-reference/#bibliography +// says that anchors like [[[ABNF]]] should render to [ANBF], but experience +// says that this is not quite true. However, [[ABNF]][ABNF] seems to render +// what I hoped for. /RL + +[[ABNF]][ABNF]:: https://datatracker.ietf.org/doc/html/rfc5234[RFC 5234 - Augmented BNF for Syntax Specifications: ABNF] +[[HTML]][HTML]:: Web Hypertext Application Technology Working Group: _HTML Living Standard_ () +[[HTML-registerhandler]][HTML-registerhandler]:: Sec. 8.9.1.4, _Custom scheme handlers: the `registerProtocolHandler()` method_ () +[[Nearley]][Nearley]:: Kartik Chandra's Nearley grammar () +[[radicle-explorer]][radicle-explorer]:: Radicle Web Interface - link:web+rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5[rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5] + + () +[[radicle-jetbrains]][radicle-jetbrains]:: Radicle JetBrains plugin - link:web+rad:z3WHS4GSf8hChLjGYfPkJY7vCxsBK[rad:z3WHS4GSf8hChLjGYfPkJY7vCxsBK] + + () +[[radicle-vscode]][radicle-vscode]:: Radicle VSCode extension - link:web+rad:z3Makm6fsQQXmpSFE43DZqwupaEhk[rad:z3Makm6fsQQXmpSFE43DZqwupaEhk] + + (), + + link:web+vscode:extension/radicle-ide-plugins-team.radicle[vscode:extension/radicle-ide-plugins-team.radicle] + + () +[[RIP2]][RIP2]:: @cloudhead: Identity - link:web+rad:z3trNYnLWS11cJWC6BbxDs5niGo82/blob/1c402116983be19e754fb14aa7ce38145f0a4b09?path=0002-identity.md[rad:z3trNYnLWS11cJWC6BbxDs5niGo82/blob/1c402116983be19e754fb14aa7ce38145f0a4b09?path=0002-identity.md] + + () +[[RIP3]][RIP3]:: @fintohaps: Radicle Improvement Proposal #3: Storage Layout - link:web+rad:z3trNYnLWS11cJWC6BbxDs5niGo82/blob/329dee9a4b65169ea3889a7da239892b705d0d68?path=0003-storage-layout.md#url[rad:z3trNYnLWS11cJWC6BbxDs5niGo82/blob/329dee9a4b65169ea3889a7da239892b705d0d68?path=0003-storage-layout.md#url] + + () +[[URI]][URI]:: https://datatracker.ietf.org/doc/html/rfc3986[RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax] +[[URI-Authority]][URI-Authority]:: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2[RFC 3986 - 3.2. Authority] +[[URI-Reference-Resolution]][URI-Reference-Resolution]:: https://datatracker.ietf.org/doc/html/rfc3986#section-5[RFC 3986 - 5. Reference Resolution] +[[zulip-mangle]][zulip-mangle]:: Zulip Topic: _`rad:` gets mangled by Zulip_ (#general, 2024-04-23) () +[[base58btc]][base58btc]:: https://datatracker.ietf.org/doc/html/draft-msporny-base58-02[Base58 Specification] +[[multibase]][multibase]:: https://www.ietf.org/archive/id/draft-multiformats-multibase-07.html[Multibase Specification] +[[multicodec]][multicodec]:: https://github.com/multiformats/multicodec/[Multiformats: multicodec] + +[appendix] +== Full <> grammar + +[%nowrap] +---- +include::data/rad-uri.abnf[tag=!rfc5234] +---- + +[appendix] +== Full <> grammar + +[%nowrap] +---- +include::data/rad-uri.ne[] +---- + +[appendix] +== Testing + +All the examples sprinkled in this RIP are tested against the <> +grammar. Test inputs and expected parse results are in `data/test/`, where +each `tc__` file contains a valid URI and the corresponding +`tc__-expected.json` contains the expected parse result as JSON. + +The test suite can be run with: + +[,shell] +---- +nix flake check +---- + +This compiles the grammar with `nearleyc`, runs `nearley-test` on each test +input, and compares the JSON output against the expected result. + +To test a single URI manually: + +[,shell] +[%nowrap] +---- +# [one time] Compile the grammar into javascript +nearleyc -o rad-uri.js data/rad-uri.ne + +# Test the chosen URI string ($URI) +nearley-test -q -i "$URI" rad-uri.js +---- + +`nearley-test` will return all possible results for the URI, which means that +where there are syntactic ambiguities, there will be more than one result. +For testing purposes, it is enough if one of the results match the expected +results linked from the footnotes associated with the examples in the tables +above. diff --git a/flake.nix b/flake.nix index 1c40814..e0ff168 100644 --- a/flake.nix +++ b/flake.nix @@ -13,11 +13,42 @@ }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; + + nearley = pkgs.buildNpmPackage { + pname = "nearley"; + version = "2.20.1"; + src = ./nix/nearley; + npmDepsHash = "sha256-a0RjAhSlzEbe6tM6QsZa06xKc0uzEe0cK6OsuVQSSzo="; + dontNpmBuild = true; + }; in { devShells.default = pkgs.mkShell { - buildInputs = [ - pkgs.asciidoctor - ]; + buildInputs = + (with pkgs; [ + asciidoctor + nodejs + shellcheck + yq + ]) + ++ [ + nearley + ]; }; + + checks.grammar = + pkgs.runCommand "grammar-check" { + nativeBuildInputs = + (with pkgs; [ + nearley + nodejs + yq + ]) + ++ [ + nearley + ]; + } '' + ${./0004-general-uri-scheme/data/test/uri-scheme-tester.sh} ${./0004-general-uri-scheme/data} + touch $out + ''; }); } diff --git a/nix/nearley/package-lock.json b/nix/nearley/package-lock.json new file mode 100644 index 0000000..a47c848 --- /dev/null +++ b/nix/nearley/package-lock.json @@ -0,0 +1,87 @@ +{ + "name": "nearley-wrapper", + "version": "2.20.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nearley-wrapper", + "version": "2.20.1", + "dependencies": { + "nearley": "^2.20.1" + }, + "bin": { + "nearley-test": "node_modules/.bin/nearley-test", + "nearleyc": "node_modules/.bin/nearleyc" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "license": "MIT" + }, + "node_modules/moo": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", + "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", + "license": "BSD-3-Clause" + }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "license": "MIT", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "license": "CC0-1.0" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "license": "MIT", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "license": "MIT", + "engines": { + "node": ">=0.12" + } + } + } +} diff --git a/nix/nearley/package.json b/nix/nearley/package.json new file mode 100644 index 0000000..a3411e6 --- /dev/null +++ b/nix/nearley/package.json @@ -0,0 +1,11 @@ +{ + "name": "nearley-wrapper", + "version": "2.20.1", + "dependencies": { + "nearley": "^2.20.1" + }, + "bin": { + "nearleyc": "./node_modules/.bin/nearleyc", + "nearley-test": "./node_modules/.bin/nearley-test" + } +}