char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
c013baf
main
1import { 2ArrayPrototypePush as push , 3RegExpPrototypeExec as exec , 4String , 5TypedArrayPrototypeGetBuffer as buffer , 6TypedArrayPrototypeGetByteLength as length , 7TypedArrayPrototypeGetByteOffset as offset , 8TypeError , 9Uint8Array , 10uncurryThis , 11WeakMapPrototypeGet as get , 12} from "ext:core/primordials" ; 13import { op_form_data_boundary , op_form_data_urlencoded } from "ext:core/ops" ; 14import { array } from "ext:web/idl.js" ; 15import { blobs , build , copyRange } from "ext:blob/blob.js" ; 16import { fileState , parsedFile } from "ext:blob/file.js" ; 17import { TextDecoder , TextEncoder } from "ext:encoding/main.js" ; 18import { forms , fromEntries } from "./form_data.js" ; 19 20import type { Part } from "ext:blob/blob.js" ; 21import type { FormData , FormEntry } from "./form_data.js" ; 22 23type Bytes = Uint8Array < ArrayBuffer >; 24 25const charCode = uncurryThis ( String . prototype . charCodeAt ), 26strSlice = uncurryThis ( String . prototype . slice ), 27strIndex = uncurryThis ( String . prototype . indexOf ), 28lower = uncurryThis ( String . prototype . toLowerCase ); 29const indexOf = uncurryThis ( Uint8Array . prototype . indexOf ); 30const decode = uncurryThis ( TextDecoder . prototype . decode ), 31encode = uncurryThis ( TextEncoder . prototype . encode ); 32const decoder = new TextDecoder ( "utf-8" , { __proto__ :null , ignoreBOM :true }), 33encoder = new TextEncoder (); 34const token = / ^[!#$%&'*+.^_`|~0-9A-Za-z-]+$ / ; 35function crlf ( value :string ) :string { 36let result = "" , start = 0 ; 37for ( let i = 0 ; i < value . length ; i ++ ) { 38const code = charCode ( value , i ); 39if ( code === 10 || code === 13 ) { 40result += strSlice ( value , start , i ) + "\r\n" ; 41if ( code === 13 && charCode ( value , i + 1 ) === 10 ) i ++ ; 42start = i + 1 ; 43} 44} 45return result + strSlice ( value , start ); 46} 47function escape ( value :string ) :string { 48let result = "" , start = 0 ; 49for ( let i = 0 ; i < value . length ; i ++ ) { 50const code = charCode ( value , i ); 51if ( code === 10 || code === 13 || code === 34 ) { 52result += strSlice ( value , start , i ) + 53( code === 10 ?"%0A" :code === 13 ?"%0D" :"%22" ); 54start = i + 1 ; 55} 56} 57return result + strSlice ( value , start ); 58} 59function serializeFormData ( value :unknown ) :{ bytes :Bytes ; type :string } | null { 60const data = get ( forms , value ); 61if ( ! data ) return null ; 62const boundary = op_form_data_boundary ( null ), parts = array < Part >(); 63for ( let i = 0 ; i < data . list . length ; i ++ ) { 64const e = data . list [ i ]; 65let header = "--" + boundary + 66'\r\nContent-Disposition: form-data; name="' + escape ( crlf ( e . name )) + '"' ; 67if ( typeof e . value === "string" ) { 68push ( parts , { 69kind :"string" , 70value :header + "\r\n\r\n" + crlf ( e . value ) + "\r\n" , 71}); 72} else { 73const f = fileState ( e . value ) ! , blob = get ( blobs , e . value ) ! ; 74header += '; filename="' + escape ( f . name ) + '"\r\nContent-Type: ' + 75( blob . type || "application/octet-stream" ) + "\r\n\r\n" ; 76push ( parts , { kind :"string" , value :header }, { 77kind :"blob" , 78value :blob , 79}, { kind :"string" , value :"\r\n" }); 80} 81} 82push ( parts , { kind :"string" , value :"--" + boundary + "--\r\n" }); 83const type = "multipart/form-data; boundary=" + boundary ; 84const body = build ( parts , { __proto__ :null , endings :"transparent" , type}); 85return { bytes :copyRange ( body , 0 , body . size ), type}; 86} 87function invalid () :never { 88throw new TypeError ( "Invalid multipart/form-data body" ); 89} 90function view ( bytes :Bytes , start :number , end :number ) :Bytes { 91return new Uint8Array ( buffer ( bytes ), offset ( bytes ) + start , end - start ); 92} 93function ows ( value :string ) :string { 94let start = 0 , end = value . length ; 95while ( charCode ( value , start ) === 9 || charCode ( value , start ) === 32 ) start ++ ; 96while ( 97end > start && 98( charCode ( value , end - 1 ) === 9 || charCode ( value , end - 1 ) === 32 ) 99) end -- ; 100return strSlice ( value , start , end ); 101} 102function disposition ( value :string ) :{ name :string ; filename :string | undefined } { 103let position = strIndex ( value , ";" ); 104const kind = ows ( position < 0 ?value :strSlice ( value , 0 , position )); 105if ( lower ( kind ) !== "form-data" ) invalid (); 106if ( position < 0 ) invalid (); 107let name :string | undefined , filename :string | undefined ; 108while ( position < value . length ) { 109position ++ ; 110while ( 111charCode ( value , position ) === 9 || charCode ( value , position ) === 32 112) position ++ ; 113if ( position === value . length ) break ; 114const start = position ; 115while ( 116position < value . length && value [ position ] !== "=" && 117value [ position ] !== ";" 118) position ++ ; 119const rawKey = ows ( strSlice ( value , start , position )); 120if ( ! exec ( token , rawKey ) || value [ position ] !== "=" ) invalid (); 121const key = lower ( rawKey ); 122position ++ ; 123while ( 124charCode ( value , position ) === 9 || charCode ( value , position ) === 32 125) position ++ ; 126let parameter ; 127if ( value [ position ] === '"' ) { 128const start = ++ position ; 129while ( position < value . length && value [ position ] !== '"' ) position ++ ; 130if ( position === value . length ) invalid (); 131parameter = strSlice ( value , start , position ++ ); 132while ( 133charCode ( value , position ) === 9 || charCode ( value , position ) === 32 134) position ++ ; 135if ( position < value . length && value [ position ] !== ";" ) invalid (); 136} else { 137const start = position ; 138while ( position < value . length && value [ position ] !== ";" ) position ++ ; 139parameter = ows ( strSlice ( value , start , position )); 140if ( ! exec ( token , parameter )) invalid (); 141} 142if ( key === "name" ) name = parameter ; 143else if ( key === "filename" ) filename = parameter ; 144} 145if ( name === undefined ) invalid (); 146return { name, filename}; 147} 148function parseMultipart ( bytes :Bytes , boundary :unknown ) :FormData { 149if ( typeof boundary !== "string" || ! boundary . length ) invalid (); 150for ( let i = 0 ; i < boundary . length ; i ++ ) { 151if ( charCode ( boundary , i ) < 32 || charCode ( boundary , i ) > 126 ) invalid (); 152} 153const prefix = encode ( encoder , "--" + boundary ), 154size = length ( bytes ), 155prefixSize = length ( prefix ); 156function matches ( position :number ) :boolean { 157if ( position + prefixSize > size ) return false ; 158for ( let i = 0 ; i < prefixSize ; i ++ ) { 159if ( bytes [ position + i ] !== prefix [ i ]) return false ; 160} 161return true ; 162} 163function delimiter ( start :number ) :{ closed :boolean ; position :number } { 164let p = start + prefixSize , closed = false ; 165if ( bytes [ p ] === 45 && bytes [ p + 1 ] === 45 ) { 166closed = true ; 167p += 2 ; 168} 169while ( bytes [ p ] === 32 || bytes [ p ] === 9 ) p ++ ; 170if ( closed && p === size ) return { closed, position :p }; 171if ( bytes [ p ] !== 13 || bytes [ p + 1 ] !== 10 ) invalid (); 172return { closed, position :p + 2 }; 173} 174function find ( from :number ) :number { 175let p = from ; 176while (( p = indexOf ( bytes , 13 , p )) !== - 1 ) { 177if ( bytes [ p + 1 ] === 10 && matches ( p + 2 )) return p ; 178p ++ ; 179} 180return - 1 ; 181} 182let start = matches ( 0 ) ?0 :find ( 0 ); 183if ( start < 0 ) invalid (); 184if ( start !== 0 || ! matches ( 0 )) start += 2 ; 185let d = delimiter ( start ); 186const list = array < FormEntry >(); 187while ( ! d . closed ) { 188let p = d . position , contentDisposition :string | undefined , contentType = "text/plain" ; 189const headerStart = p ; 190while ( true ) { 191const end = indexOf ( bytes , 13 , p ), lf = indexOf ( bytes , 10 , p ); 192if ( end < 0 || lf !== end + 1 ) invalid (); 193if ( end === p ) break ; 194p = end + 2 ; 195} 196const headers = decode ( decoder , view ( bytes , headerStart , p )); 197p += 2 ; 198let lineStart = 0 ; 199while ( lineStart < headers . length ) { 200const lineEnd = strIndex ( headers , "\r\n" , lineStart ); 201const line = strSlice ( headers , lineStart , lineEnd ); 202lineStart = lineEnd + 2 ; 203if ( charCode ( line , 0 ) === 9 || charCode ( line , 0 ) === 32 ) invalid (); 204const colon = strIndex ( line , ":" ); 205if ( colon < 0 ) invalid (); 206const rawKey = ows ( strSlice ( line , 0 , colon )), 207value = ows ( strSlice ( line , colon + 1 )); 208if ( ! exec ( token , rawKey )) invalid (); 209const key = lower ( rawKey ); 210if ( key === "content-disposition" ) contentDisposition = value ; 211else if ( key === "content-type" ) contentType = value ; 212} 213if ( contentDisposition === undefined ) invalid (); 214const metadata = disposition ( contentDisposition ), end = find ( p ); 215if ( end < 0 ) invalid (); 216const content = view ( bytes , p , end ); 217push ( list , { 218name :metadata . name , 219value :metadata . filename === undefined 220 ?decode ( decoder , content ) 221 :parsedFile ( new Uint8Array ( content ), metadata . filename , contentType ), 222}); 223d = delimiter ( end + 2 ); 224} 225return fromEntries ( list ); 226} 227function parseUrlencoded ( bytes :Bytes ) :FormData { 228const pairs = op_form_data_urlencoded ( bytes ), list = array < FormEntry >(); 229for ( let i = 0 ; i < pairs . length ; i ++ ) { 230push ( list , { name :pairs [ i ][ 0 ], value :pairs [ i ][ 1 ] }); 231} 232return fromEntries ( list ); 233} 234export { parseMultipart , parseUrlencoded , serializeFormData };