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