char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
05d50bd
main
1use url:: Url ; 2 3use crate :: config::{ self , Error , Value , ValueKind , invalid, object}; 4 5# [ derive ( Clone , Debug , PartialEq , Eq , thiserror :: Error )] 6pub enum ResolveError { 7# [ error ( "Import {0:?} is blocked by the import map" )] 8Blocked ( String ), 9# [ error ( "Import {0:?} contains an encoded path separator" )] 10EncodedSeparator ( String ), 11# [ error ( "Import {0:?} escapes its mapped prefix" )] 12EscapesPrefix ( String ), 13# [ error ( "Import subpath must be UTF-8" )] 14SubpathUtf8 , 15# [ error ( "Cannot append a subpath to import target {0}" )] 16CannotAppend ( String ), 17# [ error ( transparent )] 18Url ( # [ from ] url:: ParseError ), 19} 20 21type Mappings =Vec <( String , Option < String >)>; 22 23# [ derive ( Default , Debug )] 24pub struct ImportMap { 25imports : Mappings , 26scopes : Vec <( String , Mappings )>, 27} 28 29impl ImportMap { 30pub fn parse ( source : & str , base : & Url ) ->Result < Self , Error > { 31let root = config:: parse ( source) ?; 32let mut imports =None ; 33let mut scopes =None ; 34for memberin object ( & root) ?{ 35match member. key . as_str () { 36"imports" => imports =Some ( & member. value ), 37"scopes" => scopes =Some ( & member. value ), 38 _ =>{} 39} 40} 41Self :: from_fields ( imports, scopes, base) 42} 43 44pub ( crate ) fn from_fields ( 45imports : Option < & Value >, 46scopes : Option < & Value >, 47base : & Url , 48) ->Result < Self , Error > { 49let mut map =Self :: default (); 50if let Some ( imports) = imports{ 51 map. imports =mappings ( imports, base) ?; 52} 53if let Some ( scopes) = scopes{ 54for scopein object ( scopes) ?{ 55let prefix = base 56. join ( & scope. key ) 57. map_err ( |_|invalid ( scope. key_span . clone (), "Invalid scope URL" )) ? 58. to_string (); 59if map. scopes . iter (). any ( |( existing, _) | existing ==& prefix) { 60return Err ( invalid ( 61 scope. key_span . clone (), 62"Duplicate normalized scope URL" , 63)); 64} 65 map. scopes . push (( prefix, mappings ( & scope. value , base) ?)); 66} 67 map. scopes . sort_by_key ( |a| std:: cmp:: Reverse ( a. 0 . len ())); 68} 69Ok ( map) 70} 71 72/// Maps each bound package name, and its subpaths, to a `jsr:` package's exports. 73pub ( crate ) fn packages ( packages : impl IntoIterator < Item =( String , String )>) ->Self { 74let mut imports: Mappings = packages 75. into_iter () 76. flat_map ( |( name, package) |{ 77let slash_name =format! ( "{name}/" ); 78let slash_package =format! ( "{package}/" ); 79[( name, Some ( package)), ( slash_name, Some ( slash_package))] 80}) 81. collect (); 82 imports. sort_by_key ( |a| std:: cmp:: Reverse ( a. 0 . len ())); 83Self { imports, scopes : Vec :: new () } 84} 85 86pub ( crate ) fn namespace_conflict ( & self , name : & str ) ->Option < & str > { 87self . imports 88. iter () 89. chain ( self . scopes . iter (). flat_map ( |( _, mappings) | mappings)) 90. map ( |( key, _) | key. as_str ()) 91. find ( |key|{ 92* key == name 93 || key 94. strip_prefix ( name) 95. is_some_and ( |suffix| suffix. starts_with ( '/' )) 96 ||( key. ends_with ( '/' ) && name. starts_with ( key)) 97}) 98} 99 100pub fn resolve ( & self , specifier : & str , referrer : & Url ) ->Result < Option < String >, ResolveError > { 101let key =normalize ( specifier, referrer) ?; 102for ( prefix, mappings) in & self . scopes { 103if ( referrer. as_str () == prefix 104 ||( prefix. ends_with ( '/' ) && referrer. as_str (). starts_with ( prefix))) 105 &&let Some ( target) =apply ( mappings, & key) ? 106{ 107return Ok ( Some ( target)); 108} 109} 110apply ( & self . imports , & key) 111} 112} 113 114fn normalize ( specifier : & str , base : & Url ) ->Result < String , ResolveError > { 115if specifier. starts_with ( "./" ) || specifier. starts_with ( "../" ) || specifier. starts_with ( '/' ) { 116return base 117. join ( specifier) 118. map ( String :: from) 119. map_err ( ResolveError :: from); 120} 121if let Ok ( url) =Url :: parse ( specifier) { 122return Ok ( url. to_string ()); 123} 124Ok ( specifier. to_owned ()) 125} 126 127fn mappings ( value : & Value , base : & Url ) ->Result < Vec <( String , Option < String >)>, Error > { 128let mut mappings =Vec :: new (); 129for memberin object ( value) ?{ 130if member. key . is_empty () { 131return Err ( invalid ( 132 member. key_span . clone (), 133"Import keys must not be empty" , 134)); 135} 136let key =normalize ( & member. key , base) 137. map_err ( |error|invalid ( member. key_span . clone (), error. to_string ())) ?; 138if mappings. iter (). any ( |( existing, _) | existing ==& key) { 139return Err ( invalid ( 140 member. key_span . clone (), 141"Duplicate normalized import key" , 142)); 143} 144let target =match & member. value . kind { 145ValueKind :: Null =>None , 146ValueKind :: String ( target) =>{ 147let target =normalize ( target, base) 148. map_err ( |error|invalid ( member. value . span . clone (), error. to_string ())) ?; 149if Url :: parse ( & target). is_err () { 150return Err ( invalid ( 151 member. value . span . clone (), 152"Import targets must be URLs or relative paths" , 153)); 154} 155if key. ends_with ( '/' ) && !target. ends_with ( '/' ) { 156return Err ( invalid ( 157 member. value . span . clone (), 158"Prefix import targets must end with '/'" , 159)); 160} 161Some ( target) 162} 163 _ =>{ 164return Err ( invalid ( 165 member. value . span . clone (), 166"Import targets must be strings or null" , 167)); 168} 169}; 170 mappings. push (( key, target)); 171} 172 mappings. sort_by_key ( |a| std:: cmp:: Reverse ( a. 0 . len ())); 173Ok ( mappings) 174} 175 176fn apply ( mappings : & [( String , Option < String >)], key : & str ) ->Result < Option < String >, ResolveError > { 177for ( prefix, target) in mappings{ 178if key != prefix && !( prefix. ends_with ( '/' ) && key. starts_with ( prefix)) { 179continue ; 180} 181let target = target 182. as_ref () 183. ok_or_else ( ||ResolveError :: Blocked ( key. to_owned ())) ?; 184if key == prefix{ 185return Ok ( Some ( target. clone ())); 186} 187let suffix =& key[ prefix. len () ..]; 188let encoded = suffix. to_ascii_lowercase (); 189if encoded. contains ( "%2f" ) || encoded. contains ( "%5c" ) { 190return Err ( ResolveError :: EncodedSeparator ( key. to_owned ())); 191} 192if target. starts_with ( "jsr:" ) || target. starts_with ( "npm:" ) { 193let decoded = percent_encoding:: percent_decode_str ( suffix) 194. decode_utf8 () 195. map_err ( |_|ResolveError :: SubpathUtf8 ) ?; 196if decoded. split ( '/' ). any ( |part|matches! ( part, "." |".." )) 197 || decoded. contains ([ '?' , '#' , '\\' ]) 198{ 199return Err ( ResolveError :: EscapesPrefix ( key. to_owned ())); 200} 201return Ok ( Some ( format! ( "{target}{suffix}" ))); 202} 203let url =Url :: parse ( target) ?; 204let resolved = url 205. join ( suffix) 206. map_err ( |_|ResolveError :: CannotAppend ( target. clone ())) ?; 207if !resolved. as_str (). starts_with ( target) { 208return Err ( ResolveError :: EscapesPrefix ( key. to_owned ())); 209} 210return Ok ( Some ( resolved. to_string ())); 211} 212Ok ( None ) 213}