char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
766372c
main
1import { 2ObjectCreate , 3RangeError , 4Set , 5SetPrototypeAdd as add , 6SetPrototypeDelete as deleteKey , 7SetPrototypeHas as has , 8StringPrototypeCharCodeAt as charCodeAt , 9StringPrototypeSlice as slice , 10TypedArrayPrototypeGetBuffer as bufferOf , 11TypedArrayPrototypeGetByteLength as length , 12TypedArrayPrototypeGetByteOffset as offsetOf , 13TypedArrayPrototypeGetTag as tag , 14TypeError , 15Uint8Array as Uint8ArrayCtor , 16Uint8ArrayPrototypeSet as setBytes , 17uncurryThis , 18} from "ext:core/primordials" ; 19import { 20op_file_copy_dir_mode_sync as dirMode , 21op_file_copy_link_sync as copyLink , 22op_file_ensure_dir_sync as ensureDir , 23op_file_path_join as join , 24op_file_path_parent as parent , 25op_file_prepare_copy_sync as prepare , 26op_file_read_tree_dir_sync as readTreeDir , 27op_file_tmp_dir as tmpDir , 28op_path_platform as platform , 29} from "ext:core/ops" ; 30import { 31copyFile , 32FileHandle , 33mkdir , 34open , 35remove , 36rename , 37stat , 38} from "./sync.js" ; 39import type { CopyInfo } from "ext:core/ops" ; 40import type { CopyOptions , MakeTempOptions , WriteFileOptions } from "avril:fs" ; 41export type { CopyOptions , MakeTempOptions , WriteFileOptions } from "avril:fs" ; 42import { args , integer , path , writeOptions } from "./main.js" ; 43import { errorCode , failure , result } from "./errors.js" ; 44import { dictionary , required } from "ext:web/idl.js" ; 45 46const read = uncurryThis ( FileHandle . prototype . readSync ), 47write = uncurryThis ( FileHandle . prototype . writeSync ), 48dispose = uncurryThis ( FileHandle . prototype [ Symbol . dispose ]); 49const Encoder = globalThis . TextEncoder , Decoder = globalThis . TextDecoder ; 50const encode = uncurryThis ( Encoder . prototype . encode ), 51decode = uncurryThis ( Decoder . prototype . decode ); 52const encoder = new Encoder (), decoder = new Decoder (); 53const systemCrypto = globalThis . crypto , 54getRandomValues = uncurryThis ( systemCrypto . getRandomValues ); 55const windowsHost = platform () === "win32" ; 56const digits = "0123456789abcdef" ; 57 58function checkTempComponent ( value :string , suffix :boolean ) :void { 59for ( let i = 0 ; i < value . length ; i ++ ) { 60const code = charCodeAt ( value , i ); 61if ( code < 32 || ( code >= 127 && code <= 159 )) { 62throw failure ( 63"InvalidInput" , 64"Invalid control character in temp prefix or suffix" , 65); 66} 67if ( 68code === 34 || code === 42 || code === 47 || code === 58 || 69code === 60 || code === 62 || code === 63 || code === 92 || code === 124 70) { 71throw failure ( 72"InvalidInput" , 73"Invalid character in temp prefix or suffix" , 74); 75} 76} 77// Windows strips trailing dots and spaces, so the created name would not 78// match the returned one. 79if ( suffix && windowsHost && value . length > 0 ) { 80const last = charCodeAt ( value , value . length - 1 ); 81if ( last === 46 || last === 32 ) { 82throw failure ( 83"InvalidInput" , 84"Invalid trailing character in temp suffix" , 85); 86} 87} 88} 89 90function randomHex () :string { 91const bytes = new Uint8ArrayCtor ( 8 ); 92getRandomValues ( systemCrypto , bytes ); 93let text = "" ; 94for ( let i = 0 ; i < 8 ; i ++ ) { 95text += digits [ bytes [ i ] >> 4 ] + digits [ bytes [ i ] & 15 ]; 96} 97// Like Rust's `{value:08x}`: at least 8 digits. 98let start = 0 ; 99while ( start < 8 && text [ start ] === "0" ) start ++ ; 100return start === 0 ?text :slice ( text , start ); 101} 102 103function tempOptions ( options :unknown ) :[ string , string , string ] { 104const settings = dictionary ( options ); 105const dir = settings . dir , 106prefix = settings . prefix , 107suffix = settings . suffix ; 108const base = dir === undefined ?result ( tmpDir ()) :path ( dir ); 109if ( prefix !== undefined && typeof prefix !== "string" ) { 110throw new TypeError ( "prefix must be string" ); 111} 112if ( suffix !== undefined && typeof suffix !== "string" ) { 113throw new TypeError ( "suffix must be string" ); 114} 115const head = prefix === undefined ?"" :prefix as string ; 116const tail = suffix === undefined ?"" :suffix as string ; 117checkTempComponent ( head , false ); 118checkTempComponent ( tail , true ); 119return [ base , head , tail ]; 120} 121 122export function readFile ( pathname :string | URL ) :Uint8Array { 123required ( arguments . length , 1 ); 124const file = open ( pathname ), 125chunks = ObjectCreate ( null ) as Record < number , Uint8Array < ArrayBuffer >>; 126let count = 0 , total = 0 ; 127try { 128for (;;) { 129const buffer = new Uint8ArrayCtor ( 65536 ), 130size = read ( file , buffer ); 131if ( size === null ) break ; 132chunks [ count ++ ] = new Uint8ArrayCtor ( bufferOf ( buffer ), 0 , size ); 133total += size ; 134} 135} finally { 136dispose ( file ); 137} 138const output = new Uint8ArrayCtor ( total ); 139let offset = 0 ; 140for ( let i = 0 ; i < count ; i ++ ) { 141setBytes ( output , chunks [ i ], offset ); 142offset += length ( chunks [ i ]); 143} 144return output ; 145} 146export function readTextFile ( pathname :string | URL ) :string { 147required ( arguments . length , 1 ); 148return decode ( decoder , readFile ( pathname )) as string ; 149} 150export function writeFile ( 151pathname :string | URL , 152data :Uint8Array , 153options :WriteFileOptions | undefined = undefined , 154) :void { 155required ( arguments . length , 2 ); 156const filename = path ( pathname ); 157if ( tag ( data ) !== "Uint8Array" ) { 158throw new TypeError ( "writeFile requires a Uint8Array" ); 159} 160const snapshot = new Uint8ArrayCtor ( data as Uint8Array < ArrayBufferLike >), 161flags = writeOptions ( options ); 162const file = open ( filename , flags ); 163try { 164let offset = 0 ; 165while ( offset < length ( snapshot )) { 166const buffer = new Uint8ArrayCtor ( 167bufferOf ( snapshot ), 168offsetOf ( snapshot ) + offset , 169length ( snapshot ) - offset , 170); 171const size = write ( file , buffer ); 172if ( size <= 0 ) throw failure ( "IoError" , "File write made no progress" ); 173offset += size ; 174} 175} finally { 176dispose ( file ); 177} 178} 179export function writeTextFile ( 180pathname :string | URL , 181text :string , 182options :WriteFileOptions | undefined = undefined , 183) :void { 184required ( arguments . length , 2 ); 185if ( typeof text !== "string" ) { 186throw new TypeError ( "writeTextFile requires a string" ); 187} 188writeFile ( pathname , encode ( encoder , text ), options ); 189} 190/** False only for missing paths; permission and other errors propagate. */ 191export function exists ( pathname :string | URL ) :boolean { 192required ( arguments . length , 1 ); 193try { 194stat ( pathname ); 195return true ; 196} catch ( error ) { 197if ( errorCode ( error ) === "NotFound" ) return false ; 198throw error ; 199} 200} 201export function mkdirs ( 202pathname :string | URL , 203options :{ mode ?:number } | undefined = undefined , 204) :void { 205required ( arguments . length , 1 ); 206const filename = path ( pathname ), mode = dictionary ( options ). mode ; 207if ( mode !== undefined && integer ( mode ) > 0o7777 ) { 208throw new RangeError ( "Invalid directory mode" ); 209} 210const nativeMode = mode === undefined ?null :mode as number ; 211function ensure ( filename :string ) :void { 212try { 213result ( ensureDir ( args ( filename , nativeMode ))); 214} catch ( error ) { 215if ( errorCode ( error ) !== "NotFound" ) throw error ; 216ensure ( result < string >( parent ( filename ))); 217result ( ensureDir ( args ( filename , nativeMode ))); 218} 219} 220ensure ( filename ); 221} 222/** Creates a uniquely-named empty directory with mode 0o700; the caller removes it. Collisions retry; missing parents are an error. */ 223export function makeTempDir ( 224options :MakeTempOptions | undefined = undefined , 225) :string { 226const [ base , prefix , suffix ] = tempOptions ( options ); 227for ( let attempt = 0 ; attempt < 10 ; attempt ++ ) { 228const candidate = result ( join ( args ( base , prefix + randomHex () + suffix ))); 229try { 230mkdir ( candidate , { mode :0o700 }); 231return candidate ; 232} catch ( error ) { 233if ( errorCode ( error ) !== "AlreadyExists" ) throw error ; 234} 235} 236throw failure ( "AlreadyExists" , "too many temp dirs exist" ); 237} 238/** Creates a uniquely-named empty file with mode 0o600; the caller removes it. Collisions retry; missing parents are an error. */ 239export function makeTempFile ( 240options :MakeTempOptions | undefined = undefined , 241) :string { 242const [ base , prefix , suffix ] = tempOptions ( options ); 243for ( let attempt = 0 ; attempt < 10 ; attempt ++ ) { 244const candidate = result ( join ( args ( base , prefix + randomHex () + suffix ))); 245try { 246const file = open ( candidate , { 247write :true , 248createNew :true , 249mode :0o600 , 250}); 251dispose ( file ); 252return candidate ; 253} catch ( error ) { 254if ( errorCode ( error ) !== "AlreadyExists" ) throw error ; 255} 256} 257throw failure ( "AlreadyExists" , "too many temp files exist" ); 258} 259/** Preserves symlinks by default and ordinary mode bits, including existing destination directories. Following links rejects ancestor cycles but may duplicate shared subtrees. */ 260export function copy ( 261source :string | URL , 262destination :string | URL , 263options :CopyOptions | undefined = undefined , 264) :void { 265required ( arguments . length , 2 ); 266const from = path ( source ), 267to = path ( destination ), 268dereference = dictionary ( options ). dereference ; 269if ( dereference !== undefined && typeof dereference !== "boolean" ) { 270throw new TypeError ( "dereference must be boolean" ); 271} 272const ancestors = new Set < string >(); 273function visit ( source :string , destination :string ) :void { 274const info = result < CopyInfo >( 275prepare ( args ( source , destination , false , dereference === true )), 276); 277const actualSource = info [ 3 ]; 278if ( info [ 0 ] === 1 ) { 279if ( has ( ancestors , info [ 2 ])) { 280throw failure ( 281"InvalidInput" , 282"Directory cycle while copying symbolic links" , 283); 284} 285add ( ancestors , info [ 2 ]); 286try { 287mkdirs ( destination , { mode :0o700 }); 288result ( dirMode ( args ( destination , 0o700 ))); 289const entries = result < string []>( readTreeDir ( actualSource )); 290for ( let i = 0 ; i < entries . length ; i ++ ) { 291visit ( 292result ( join ( args ( actualSource , entries [ i ][ 0 ]))), 293result ( join ( args ( destination , entries [ i ][ 0 ]))), 294); 295} 296result ( dirMode ( args ( destination , info [ 1 ]))); 297} finally { 298deleteKey ( ancestors , info [ 2 ]); 299} 300} else { 301const transfer = () :void => { 302if ( info [ 0 ] === 2 ) { 303result ( copyLink ( args ( actualSource , destination ))); 304} else copyFile ( actualSource , destination ); 305}; 306try { 307transfer (); 308} catch ( error ) { 309if ( errorCode ( error ) !== "NotFound" ) throw error ; 310mkdirs ( result ( parent ( destination ))); 311transfer (); 312} 313} 314} 315visit ( from , to ); 316} 317/** Rename first; cross-device fallback copies then removes and is not atomic. */ 318export function move ( source :string | URL , destination :string | URL ) :void { 319required ( arguments . length , 2 ); 320const from = path ( source ), to = path ( destination ); 321try { 322rename ( from , to ); 323return ; 324} catch ( error ) { 325if ( errorCode ( error ) !== "CrossDevice" ) throw error ; 326} 327const directory = 328result < CopyInfo >( prepare ( args ( from , to , true , false )))[ 0 ] === 1 ; 329copy ( from , to ); 330result ( prepare ( args ( from , to , true , false ))); 331remove ( from , { recursive :directory }); 332}