char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
ec742cf
main
1use std:: collections:: HashMap ; 2use std:: io::{ self , Write }; 3use std:: panic::{ AssertUnwindSafe , catch_unwind}; 4use std:: sync:: atomic::{ AtomicU64 , Ordering }; 5use std:: sync::{ Arc , Mutex , OnceLock }; 6 7use avril_core:: serde_v8::{ JsBuffer , ToJsBuffer }; 8use avril_core::{ Extension , Op }; 9use brotli::{ CompressorWriter , DecompressorWriter }; 10use flate2::{ Compress , Compression , Decompress , FlushCompress , FlushDecompress , Status }; 11use futures:: channel:: oneshot; 12 13const MAX_ID : u64 =9_007_199_254_740_991 ; 14const OUTPUT_BUFFER_SIZE : usize =16 * 1024 ; 15 16struct CodecOutput { 17bytes : Vec < u8 >, 18terminal_error : bool , 19} 20 21impl CodecOutput { 22fn success ( bytes : Vec < u8 >) ->Self { 23Self { bytes, terminal_error : false } 24} 25} 26 27# [ derive ( Default )] 28struct OutputSink ( Vec < u8 >); 29 30impl Write for OutputSink { 31fn write ( & mut self , bytes : & [ u8 ]) -> io:: Result < usize > { 32self . 0 . extend_from_slice ( bytes); 33Ok ( bytes. len ()) 34} 35 36fn flush ( & mut self ) -> io:: Result <()> { 37Ok (()) 38} 39} 40 41enum Codec { 42Compress { 43codec : Compress , 44finished : bool , 45}, 46Decompress { 47codec : Decompress , 48finished : bool , 49}, 50BrotliCompress { 51codec : Option < Box < CompressorWriter < OutputSink >>>, 52finished : bool , 53}, 54BrotliDecompress { 55codec : Option < Box < DecompressorWriter < OutputSink >>>, 56finished : bool , 57}, 58} 59 60// Brotli writer destructors can finish buffered input after a future is canceled. 61impl Drop for Codec { 62fn drop ( & mut self ) { 63match self { 64Self :: BrotliCompress { codec, ..} =>{ 65if let Some ( writer) = codec. take () { 66spawn_compression ( move ||drop ( writer)); 67} 68} 69Self :: BrotliDecompress { codec, ..} =>{ 70if let Some ( writer) = codec. take () { 71spawn_compression ( move ||drop ( writer)); 72} 73} 74 _ =>{} 75} 76} 77} 78 79impl Codec { 80fn new ( format : & str , decompress : bool ) -> io:: Result < Self > { 81// Brotli state is created by worker operations, not on the isolate thread. 82if format =="brotli" { 83return Ok ( if decompress{ 84Self :: BrotliDecompress { codec : None , finished : false } 85} else { 86Self :: BrotliCompress { codec : None , finished : false } 87}); 88} 89if decompress{ 90let codec =match format{ 91"deflate" =>Decompress :: new ( true ), 92"deflate-raw" =>Decompress :: new ( false ), 93"gzip" =>Decompress :: new_gzip ( 15 ), 94 _ =>return Err ( io:: Error :: other ( "Unsupported compression format" )), 95}; 96Ok ( Self :: Decompress { codec, finished : false }) 97} else { 98let codec =match format{ 99"deflate" =>Compress :: new ( Compression :: default (), true ), 100"deflate-raw" =>Compress :: new ( Compression :: default (), false ), 101"gzip" =>Compress :: new_gzip ( Compression :: default (), 15 ), 102 _ =>return Err ( io:: Error :: other ( "Unsupported compression format" )), 103}; 104Ok ( Self :: Compress { codec, finished : false }) 105} 106} 107 108fn transform ( & mut self , input : & [ u8 ]) -> io:: Result < CodecOutput > { 109if input. is_empty () { 110return Ok ( CodecOutput :: success ( Vec :: new ())); 111} 112match self { 113Self :: Compress { codec, finished} =>{ 114if * finished{ 115return Err ( io:: Error :: other ( "Compression stream is closed" )); 116} 117run_compress ( codec, input, FlushCompress :: None , false ). map ( CodecOutput :: success) 118} 119Self :: Decompress { codec, finished} =>{ 120if * finished{ 121return Ok ( CodecOutput { 122bytes : Vec :: new (), 123terminal_error : true , 124}); 125} 126let ( bytes, ended, trailing) =run_decompress ( codec, input, FlushDecompress :: None ) ?; 127if ended || trailing{ 128* finished =true ; 129} 130Ok ( CodecOutput { bytes, terminal_error : trailing}) 131} 132Self :: BrotliCompress { codec, finished} =>{ 133if * finished{ 134return Err ( io:: Error :: other ( "Compression stream is closed" )); 135} 136let writer = codec. get_or_insert_with ( ||{ 137Box :: new ( CompressorWriter :: new ( 138OutputSink :: default (), 139OUTPUT_BUFFER_SIZE , 1405 , 14122 , 142)) 143}); 144 writer. write_all ( input) ?; 145Ok ( CodecOutput :: success ( std:: mem:: take ( 146& mut writer. get_mut (). 0 , 147))) 148} 149Self :: BrotliDecompress { codec, finished} =>{ 150if * finished{ 151return Ok ( CodecOutput { 152bytes : Vec :: new (), 153terminal_error : true , 154}); 155} 156let writer = codec. get_or_insert_with ( ||{ 157Box :: new ( DecompressorWriter :: new ( 158OutputSink :: default (), 159OUTPUT_BUFFER_SIZE , 160)) 161}); 162let consumed = writer. write ( input) ?; 163let bytes = std:: mem:: take ( & mut writer. get_mut (). 0 ); 164let trailing = consumed < input. len (); 165if trailing{ 166* finished =true ; 167} 168Ok ( CodecOutput { bytes, terminal_error : trailing}) 169} 170} 171} 172 173fn finish ( & mut self ) -> io:: Result < CodecOutput > { 174match self { 175Self :: Compress { codec, finished} =>{ 176if * finished{ 177return Ok ( CodecOutput :: success ( Vec :: new ())); 178} 179let output =run_compress ( codec, & [], FlushCompress :: Finish , true ) ?; 180* finished =true ; 181Ok ( CodecOutput :: success ( output)) 182} 183Self :: Decompress { codec, finished} =>{ 184if * finished{ 185return Ok ( CodecOutput :: success ( Vec :: new ())); 186} 187let ( output, ended, _) =run_decompress ( codec, & [], FlushDecompress :: Finish ) ?; 188if !ended{ 189return Err ( io:: Error :: other ( "Truncated compressed stream" )); 190} 191* finished =true ; 192Ok ( CodecOutput :: success ( output)) 193} 194Self :: BrotliCompress { codec, finished} =>{ 195if * finished{ 196return Ok ( CodecOutput :: success ( Vec :: new ())); 197} 198let writer = codec. take (). unwrap_or_else ( ||{ 199Box :: new ( CompressorWriter :: new ( 200OutputSink :: default (), 201OUTPUT_BUFFER_SIZE , 2025 , 20322 , 204)) 205}); 206* finished =true ; 207Ok ( CodecOutput :: success (( * writer). into_inner (). 0 )) 208} 209Self :: BrotliDecompress { codec, finished} =>{ 210if * finished{ 211return Ok ( CodecOutput :: success ( Vec :: new ())); 212} 213let writer = codec. take (). unwrap_or_else ( ||{ 214Box :: new ( DecompressorWriter :: new ( 215OutputSink :: default (), 216OUTPUT_BUFFER_SIZE , 217)) 218}); 219let output =( * writer) 220. into_inner () 221. map_err ( |_| io:: Error :: other ( "Truncated Brotli stream" )) ?; 222* finished =true ; 223Ok ( CodecOutput :: success ( output. 0 )) 224} 225} 226} 227} 228 229fn run_compress ( 230codec : & mut Compress , 231input : & [ u8 ], 232flush : FlushCompress , 233finishing : bool , 234) -> io:: Result < Vec < u8 >> { 235let mut output =Vec :: new (); 236let mut consumed =0 ; 237loop { 238let before_in = codec. total_in (); 239let before_out = codec. total_out (); 240let mut buffer =[ 0 ; OUTPUT_BUFFER_SIZE ]; 241let status = codec 242. compress ( & input[ consumed..], & mut buffer, flush) 243. map_err ( io:: Error :: other) ?; 244 consumed +=( codec. total_in () - before_in) as usize ; 245let produced =( codec. total_out () - before_out) as usize ; 246 output. try_reserve ( produced). map_err ( io:: Error :: other) ?; 247 output. extend_from_slice ( & buffer[ ..produced]); 248if status ==Status :: StreamEnd { 249if consumed != input. len () { 250return Err ( io:: Error :: other ( 251"Compression ended before input was consumed" , 252)); 253} 254return Ok ( output); 255} 256if consumed == input. len () && produced < buffer. len () && !finishing{ 257return Ok ( output); 258} 259if codec. total_in () == before_in && codec. total_out () == before_out{ 260return Err ( io:: Error :: other ( "Compression made no progress" )); 261} 262} 263} 264 265fn run_decompress ( 266codec : & mut Decompress , 267input : & [ u8 ], 268flush : FlushDecompress , 269) -> io:: Result <( Vec < u8 >, bool , bool )> { 270let mut output =Vec :: new (); 271let mut consumed =0 ; 272loop { 273let before_in = codec. total_in (); 274let before_out = codec. total_out (); 275let mut buffer =[ 0 ; OUTPUT_BUFFER_SIZE ]; 276let status = codec 277. decompress ( & input[ consumed..], & mut buffer, flush) 278. map_err ( io:: Error :: other) ?; 279 consumed +=( codec. total_in () - before_in) as usize ; 280let produced =( codec. total_out () - before_out) as usize ; 281 output. try_reserve ( produced). map_err ( io:: Error :: other) ?; 282 output. extend_from_slice ( & buffer[ ..produced]); 283if status ==Status :: StreamEnd { 284return Ok (( output, true , consumed != input. len ())); 285} 286if consumed == input. len () && produced < buffer. len () && flush !=FlushDecompress :: Finish { 287return Ok (( output, false , false )); 288} 289if codec. total_in () == before_in && codec. total_out () == before_out{ 290if flush ==FlushDecompress :: Finish || consumed == input. len () { 291return Ok (( output, false , false )); 292} 293return Err ( io:: Error :: other ( "Decompression made no progress" )); 294} 295} 296} 297 298# [ derive ( Default )] 299struct CodecStore { 300next_id : AtomicU64 , 301entries : Mutex < HashMap < u64 , Arc < Mutex < Codec >>>>, 302} 303 304impl CodecStore { 305fn insert ( & self , codec : Codec ) -> io:: Result < u64 > { 306let id =self 307. next_id 308. fetch_update ( Ordering :: Relaxed , Ordering :: Relaxed , |id|{ 309( id <MAX_ID ). then_some ( id +1 ) 310}) 311. map ( |id| id +1 ) 312. map_err ( |_| io:: Error :: other ( "Compression resource IDs exhausted" )) ?; 313self . entries 314. lock () 315. map_err ( |_| io:: Error :: other ( "Compression registry poisoned" )) ? 316. insert ( id, Arc :: new ( Mutex :: new ( codec))); 317Ok ( id) 318} 319 320fn get ( & self , id : u64 ) -> io:: Result < Arc < Mutex < Codec >>> { 321self . entries 322. lock () 323. map_err ( |_| io:: Error :: other ( "Compression registry poisoned" )) ? 324. get ( & id) 325. cloned () 326. ok_or_else ( || io:: Error :: other ( "Compression stream is closed" )) 327} 328 329fn remove ( & self , id : u64 ) { 330if let Ok ( mut entries) =self . entries . lock () { 331 entries. remove ( & id); 332} 333} 334} 335 336fn spawn_compression ( job : impl FnOnce () +Send +' static ) { 337compression_pool (). spawn ( move ||{ 338let _ =catch_unwind ( AssertUnwindSafe ( job)); 339}); 340} 341 342fn compression_pool () ->&' static rayon:: ThreadPool { 343static POOL : OnceLock < rayon:: ThreadPool > =OnceLock :: new (); 344POOL . get_or_init ( ||{ 345 rayon:: ThreadPoolBuilder :: new () 346. num_threads ( 4 ) 347. thread_name ( |index|format! ( "avril:compression:{index}" )) 348. build () 349. expect ( "failed to initialize compression worker pool" ) 350}) 351} 352 353async fn run_operation ( 354codec : Arc < Mutex < Codec >>, 355input : Option < Vec < u8 >>, 356finish : bool , 357id : u64 , 358store : Arc < CodecStore >, 359) -> io:: Result <( ToJsBuffer , bool )> { 360let ( sender, receiver) = oneshot:: channel (); 361spawn_compression ( move ||{ 362let result =catch_unwind ( AssertUnwindSafe ( ||{ 363let mut codec = codec 364. lock () 365. map_err ( |_| io:: Error :: other ( "Compression codec poisoned" )) ?; 366match input{ 367Some ( input) => codec. transform ( & input), 368None => codec. finish (), 369} 370})) 371. unwrap_or_else ( |_|Err ( io:: Error :: other ( "Compression worker panicked" ))); 372if finish || result. is_err () || result. as_ref (). is_ok_and ( |output| output. terminal_error ) { 373 store. remove ( id); 374} 375let _ = sender. send ( result); 376}); 377let output = receiver. await . map_err ( io:: Error :: other) ??; 378Ok (( ToJsBuffer :: from ( output. bytes ), output. terminal_error )) 379} 380 381pub fn init () ->Extension { 382let store =Arc :: new ( CodecStore :: default ()); 383let new_store = store. clone (); 384let operation_store = store. clone (); 385let finish_store = store. clone (); 386let dispose_store = store. clone (); 387Extension { 388name : "ext:compression" , 389dependencies : & [ "ext:web" , "ext:webstreams" ], 390ops : vec! [ 391Op :: new ( 392"op_compression_new" , 393move |( format, decompress): ( String , bool ) |{ 394 new_store. insert ( Codec :: new ( & format, decompress) ?) 395}, 396), 397Op :: asynchronous ( 398"op_compression_transform" , 399move |( id, input): ( u64 , JsBuffer ) |{ 400let input = input. as_ref (). to_vec (); 401let codec = operation_store. get ( id); 402let store = operation_store. clone (); 403async move { 404let codec = codec?; 405run_operation ( codec, Some ( input), false , id, store). await 406} 407}, 408), 409Op :: asynchronous ( "op_compression_finish" , move |id : u64 |{ 410let codec = finish_store. get ( id); 411let store = finish_store. clone (); 412async move { 413let codec = codec?; 414run_operation ( codec, None , true , id, store). await 415} 416}), 417Op :: new ( "op_compression_dispose" , move |id : u64 |{ 418 dispose_store. remove ( id); 419Ok ::< _ , io:: Error >(()) 420}), 421], 422modules : & [( 423"ext:compression/main.js" , 424include_str! ( concat! ( env! ( "OUT_DIR" ), "/main.js" )), 425)], 426entry_point : Some ( "ext:compression/main.js" ), 427} 428}