@@ -11,21 +11,29 @@ import { isValidNsid } from './nsid'
1111// - optionally, follow "authority" with "/" and valid NSID as start of path
1212// - optionally, if NSID given, follow that with "/" and rkey
1313// - rkey path component can include URL-encoded ("percent encoded"), or:
14- // ALPHA / DIGIT / "-" / "." / "_" / "~" / ":" / "@" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
15- // [a-zA-Z0-9._~:@!$&'\(\)*+,;=-]
14+ // ALPHA / DIGIT / "-" / "." / "_" / "~" / ":" / "@" / "!" / "$" / "&" / "?" / " '" / "(" / ")" / "*" / "+" / "," / ";" / "="
15+ // [a-zA-Z0-9._~:@!$&? '\(\)*+,;=-]
1616// - rkey must have at least one char
1717// - regardless of path component, a fragment can follow as "#" and then a JSON pointer (RFC-6901)
1818export const ensureValidAtUri = ( uri : string ) => {
1919 // JSON pointer is pretty different from rest of URI, so split that out first
20- const uriParts = uri . split ( '#' )
21- if ( uriParts . length > 2 ) {
22- throw new Error ( 'ATURI can have at most one "#", separating fragment out' )
20+ let fragmentPart : string | null = null
21+ const hashIndex = uri . indexOf ( '#' )
22+ if ( hashIndex > - 1 ) {
23+ fragmentPart = uri . slice ( hashIndex + 1 )
24+ uri = uri . slice ( 0 , hashIndex )
25+ }
26+
27+ // query part is also pretty different, so split that out next
28+ let queryPart : string | null = null
29+ const queryIndex = uri . indexOf ( '?' )
30+ if ( queryIndex > - 1 ) {
31+ queryPart = uri . slice ( queryIndex + 1 )
32+ uri = uri . slice ( 0 , queryIndex )
2333 }
24- const fragmentPart = uriParts [ 1 ] || null
25- uri = uriParts [ 0 ]
2634
2735 // check that all chars are boring ASCII
28- if ( ! / ^ [ a - z A - Z 0 - 9 . _ ~ : @ ! $ & ' ) ( * + , ; = % / - ] * $ / . test ( uri ) ) {
36+ if ( ! / ^ [ a - z A - Z 0 - 9 . _ ~ : @ ! $ & ? ' ) ( * + , ; = % / - ] * $ / . test ( uri ) ) {
2937 throw new Error ( 'Disallowed characters in ATURI (ASCII)' )
3038 }
3139
@@ -75,9 +83,6 @@ export const ensureValidAtUri = (uri: string) => {
7583 )
7684 }
7785
78- if ( uriParts . length >= 2 && fragmentPart == null ) {
79- throw new Error ( 'ATURI fragment must be non-empty and start with slash' )
80- }
8186
8287 if ( fragmentPart != null ) {
8388 if ( fragmentPart . length === 0 || fragmentPart [ 0 ] !== '/' ) {
@@ -89,6 +94,16 @@ export const ensureValidAtUri = (uri: string) => {
8994 }
9095 }
9196
97+ if ( queryPart != null ) {
98+ if ( queryPart . length === 0 ) {
99+ throw new Error ( 'ATURI query must be non-empty' )
100+ }
101+ // NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace
102+ if ( ! / ^ [ a - z A - Z 0 - 9 . _ ~ : @ ! $ & ? ' ) ( * + , ; = % / - ] * $ / . test ( queryPart ) ) {
103+ throw new Error ( 'Disallowed characters in ATURI query (ASCII)' )
104+ }
105+ }
106+
92107 if ( uri . length > 8 * 1024 ) {
93108 throw new Error ( 'ATURI is far too long' )
94109 }
@@ -98,7 +113,7 @@ export const ensureValidAtUriRegex = (uri: string): void => {
98113 // simple regex to enforce most constraints via just regex and length.
99114 // hand wrote this regex based on above constraints. whew!
100115 const aturiRegex =
101- / ^ a t : \/ \/ (?< authority > [ a - z A - Z 0 - 9 . _ : % - ] + ) ( \/ (?< collection > [ a - z A - Z 0 - 9 - .] + ) ( \/ (?< rkey > [ a - z A - Z 0 - 9 . _ ~ : @ ! $ & % ' ) ( * + , ; = - ] + ) ) ? ) ? ( # (?< fragment > \/ [ a - z A - Z 0 - 9 . _ ~ : @ ! $ & % ' ) ( * + , ; = \- [ \] / \\ ] * ) ) ? $ /
116+ / ^ a t : \/ \/ (?< authority > [ a - z A - Z 0 - 9 . _ : % - ] + ) ( \/ (?< collection > [ a - z A - Z 0 - 9 - .] + ) ( \/ (?< rkey > [ a - z A - Z 0 - 9 . _ ~ : @ ! $ & % ' ) ( * + , ; = - ] + ) ) ? ) ? ( \? (?< query > [ a - z A - Z 0 - 9 . _ ~ : @ ! $ & % ' ) ( * + , ; = / - ] * ) ) ? ( # (?< fragment > \/ [ a - z A - Z 0 - 9 . _ ~ : @ ! $ & % ' ) ( * + , ; = \- [ \] / ] * ) ) ? $ /
102117 const rm = uri . match ( aturiRegex )
103118 if ( ! rm || ! rm . groups ) {
104119 throw new Error ( "ATURI didn't validate via regex" )
0 commit comments