1use std::ops::RangeInclusive;
4
5use crate::stream_web::{
6 readable_byte_stream_controller_close_stream,
7 readable_byte_stream_controller_enqueue_bytes_borrowed, utils::promise::PromisePrimordials,
8 CancelAlgorithm, PullAlgorithm, ReadableStream, ReadableStreamControllerClass,
9};
10use crate::utils::{
11 array_buffer::shared_array_buffer_view,
12 bytes::{get_lossy_string, ObjectBytes},
13 object::not_a_object_error,
14 primordials::Primordial,
15 result::ResultExt,
16 string::get_coerced_defined_string,
17};
18use rquickjs::{
19 atom::PredefinedAtom, class::Trace, function::Opt, prelude::This, Array, ArrayBuffer, Class,
20 Coerced, Ctx, Exception, FromJs, IntoJs, JsIterator, Result, TypedArray, Value,
21};
22
23use super::file::File;
24
25struct ArrayPartsIter<'js> {
26 array: Array<'js>,
27 index: usize,
28}
29
30impl<'js> ArrayPartsIter<'js> {
31 fn new(array: Array<'js>) -> Self {
32 Self { array, index: 0 }
33 }
34}
35
36impl<'js> Iterator for ArrayPartsIter<'js> {
37 type Item = Result<Value<'js>>;
38
39 fn next(&mut self) -> Option<Self::Item> {
40 let len: usize = match self.array.as_object().get(PredefinedAtom::Length) {
41 Ok(v) => v,
42 Err(e) => return Some(Err(e)),
43 };
44 if self.index >= len {
45 return None;
46 }
47 let result = self.array.get(self.index);
48 self.index += 1;
49 Some(result)
50 }
51}
52
53enum EndingType {
54 Native,
55 Transparent,
56}
57
58#[cfg(windows)]
59const LINE_ENDING: &[u8] = b"\r\n";
60#[cfg(not(windows))]
61const LINE_ENDING: &[u8] = b"\n";
62
63#[rquickjs::class]
64#[derive(Trace, Clone, rquickjs::JsLifetime)]
65pub struct Blob<'js> {
66 data: ArrayBuffer<'js>,
69 mime_type: String,
70}
71
72fn normalize_type(mut mime_type: String) -> String {
73 static INVALID_RANGE: RangeInclusive<u8> = 0x0020..=0x007E;
74
75 let bytes = unsafe { mime_type.as_bytes_mut() };
76 for byte in bytes {
77 if !INVALID_RANGE.contains(byte) {
78 return String::new();
79 }
80 byte.make_ascii_lowercase();
81 }
82 mime_type
83}
84
85#[rquickjs::methods]
86impl<'js> Blob<'js> {
87 #[qjs(constructor)]
88 pub fn new(
89 ctx: Ctx<'js>,
90 this: This<Value<'js>>,
91 parts: Opt<Value<'js>>,
92 options: Opt<Value<'js>>,
93 ) -> Result<Self> {
94 if this.as_function().is_none() {
95 return Err(Exception::throw_type(
96 &ctx,
97 "Failed to construct 'Blob': Please use the 'new' operator",
98 ));
99 }
100
101 Self::from_parts(ctx, parts, options)
102 }
103
104 #[qjs(get)]
105 pub fn size(&self) -> usize {
106 self.data.len()
107 }
108
109 #[qjs(get, rename = "type")]
110 pub fn mime_type(&self) -> String {
111 self.mime_type.clone()
112 }
113
114 pub async fn text(&self) -> String {
115 String::from_utf8_lossy(self.as_bytes()).to_string()
116 }
117
118 #[qjs(rename = "arrayBuffer")]
119 pub async fn array_buffer(&self, ctx: Ctx<'js>) -> Result<ArrayBuffer<'js>> {
120 ArrayBuffer::new_copy(ctx, self.as_bytes())
122 }
123
124 pub async fn bytes(&self, ctx: Ctx<'js>) -> Result<Value<'js>> {
125 let ab = ArrayBuffer::new_copy(ctx, self.as_bytes())?;
127 TypedArray::<u8>::from_arraybuffer(ab).map(|t| t.into_value())
128 }
129
130 pub fn slice(
131 &self,
132 ctx: Ctx<'js>,
133 start: Opt<Value<'js>>,
134 end: Opt<Value<'js>>,
135 content_type: Opt<Value<'js>>,
136 ) -> Result<Blob<'js>> {
137 let start = start.0.and_then(|v| v.as_number()).map(clamp_long_long);
138 let end = end.0.and_then(|v| v.as_number()).map(clamp_long_long);
139 Self::slice_blob(self, &ctx, start, end, content_type.0)
140 }
141
142 pub fn stream(&self, ctx: Ctx<'js>) -> Result<Value<'js>> {
143 let data = self.get_bytes();
150 let pull = PullAlgorithm::from_fn_once(
151 move |ctx: Ctx<'js>, controller: ReadableStreamControllerClass<'js>| {
152 let ctrl = match controller {
153 ReadableStreamControllerClass::ReadableStreamByteController(c) => c,
154 _ => return Err(Exception::throw_type(&ctx, "Expected byte controller")),
155 };
156 let len = data.len();
157 if len != 0 {
158 let buffer = ArrayBuffer::new(ctx.clone(), data.clone())?;
159 let view = shared_array_buffer_view(&ctx, &buffer, 0, len)?;
160 readable_byte_stream_controller_enqueue_bytes_borrowed(
161 ctx.clone(),
162 ctrl.clone(),
163 view,
164 )?;
165 }
166 readable_byte_stream_controller_close_stream(ctx.clone(), ctrl)?;
167 Ok(PromisePrimordials::get(&ctx)?
168 .promise_resolved_with_undefined
169 .clone())
170 },
171 );
172 let stream = ReadableStream::from_byte_pull_algorithm(
175 ctx,
176 pull,
177 CancelAlgorithm::ReturnPromiseUndefined,
178 )?;
179 Ok(stream.into_value())
180 }
181
182 #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
183 pub fn to_string_tag() -> &'static str {
184 stringify!(Blob)
185 }
186
187 #[qjs(static, rename = PredefinedAtom::SymbolHasInstance)]
188 pub fn has_instance(value: Value<'js>) -> bool {
189 if let Some(obj) = value.as_object() {
190 return obj.instance_of::<Self>() || obj.instance_of::<File>();
191 }
192 false
193 }
194
195 #[qjs(skip)]
196 pub fn slice_blob(
197 &self,
198 ctx: &Ctx<'js>,
199 start: Option<isize>,
200 end: Option<isize>,
201 content_type: Option<Value<'js>>,
202 ) -> Result<Blob<'js>> {
203 let bytes = self.as_bytes();
204 let len = bytes.len();
205 let start = start.unwrap_or_default();
206 let start = if start < 0 {
207 (len as isize + start).max(0) as usize
208 } else {
209 len.min(start as usize)
210 };
211 let end = end.unwrap_or(len as isize);
212 let end = if end < 0 {
213 (len as isize + end).max(0) as usize
214 } else {
215 len.min(end as usize)
216 };
217 let data = shared_array_buffer_view(ctx, &self.data, start, end.saturating_sub(start))?;
218 let mime_type = get_coerced_defined_string(&content_type);
219 let mime_type = mime_type.map(normalize_type).unwrap_or_default();
220 Ok(Blob { mime_type, data })
221 }
222}
223
224impl<'js> Blob<'js> {
225 pub fn from_bytes(ctx: &Ctx<'js>, data: Vec<u8>, content_type: Option<String>) -> Result<Self> {
226 let mime_type = content_type.map(normalize_type).unwrap_or_default();
227 let data = ArrayBuffer::new(ctx.clone(), data)?;
228 Ok(Self { mime_type, data })
229 }
230
231 pub fn from_parts(
232 ctx: Ctx<'js>,
233 parts: Opt<Value<'js>>,
234 options: Opt<Value<'js>>,
235 ) -> Result<Self> {
236 if let Some(options) = options.0.as_ref() {
237 if !options.is_null() && !options.is_undefined() && options.as_object().is_none() {
238 return Err(not_a_object_error(&ctx, "options"));
239 }
240 }
241
242 let mut endings = EndingType::Transparent;
243 if let Some(options) = options.0.as_ref() {
244 if let Some(opts) = options.as_object() {
245 if opts.contains_key("endings")? {
246 if let Some(parsed) = parse_endings(&ctx, opts.get("endings")?)? {
247 endings = parsed;
248 }
249 }
250 }
251 }
252
253 let bytes = if let Some(parts) = parts.0 {
254 bytes_from_parts(&ctx, parts, endings)?
255 } else {
256 Vec::new()
257 };
258
259 let mut mime_type = String::new();
260 if let Some(options) = options.0.as_ref() {
261 if let Some(opts) = options.as_object() {
262 if let Some(x) = opts.get::<_, Option<Coerced<String>>>("type")? {
263 mime_type = normalize_type(x.to_string());
264 }
265 }
266 }
267
268 let data = ArrayBuffer::new(ctx, bytes)?;
271
272 Ok(Self { data, mime_type })
273 }
274
275 pub fn get_bytes(&self) -> Vec<u8> {
276 self.as_bytes().to_vec()
277 }
278
279 pub fn array_buffer_ref(&self) -> ArrayBuffer<'js> {
284 self.data.clone()
285 }
286
287 pub fn as_bytes(&self) -> &[u8] {
293 unsafe { self.data.as_bytes() }.unwrap_or(&[])
295 }
296}
297
298fn bytes_from_parts<'js>(
299 ctx: &Ctx<'js>,
300 parts: Value<'js>,
301 endings: EndingType,
302) -> Result<Vec<u8>> {
303 if parts.is_undefined() {
304 return Ok(Vec::new());
305 }
306
307 if let Some(array) = parts.clone().into_array() {
308 return process_parts(ctx, ArrayPartsIter::new(array), endings);
309 }
310
311 process_parts(ctx, JsIterator::from_js(ctx, parts)?, endings)
312}
313
314fn process_parts<'js, I>(ctx: &Ctx<'js>, iter: I, endings: EndingType) -> Result<Vec<u8>>
315where
316 I: IntoIterator<Item = Result<Value<'js>>>,
317{
318 let mut data = Vec::new();
319 for elem in iter {
320 let elem = elem?;
321 if let Some(arr) = elem.as_array() {
322 let string = array_to_string(arr)?;
323 data.extend_from_slice(string.as_bytes());
324 continue;
325 }
326 if let Some(object) = elem.as_object() {
327 if let Some(x) = Class::<Blob>::from_object(object) {
328 data.extend_from_slice(x.borrow().as_bytes());
329 continue;
330 }
331 if let Some(x) = Class::<File>::from_object(object) {
332 let file = x.borrow();
333 let end = Some(file.size().try_into().or_throw(ctx)?);
334 let mime_type = Some(file.mime_type().into_js(ctx)?);
335 let sub = file.slice(ctx.clone(), Opt(Some(0)), Opt(end), Opt(mime_type))?;
336 data.extend_from_slice(sub.as_bytes());
337 continue;
338 }
339 if let Ok(x) = ObjectBytes::from(ctx, object) {
340 data.extend_from_slice(x.as_bytes(ctx).map_err(|_| {
341 Exception::throw_type(ctx, "Cannot create a blob with detached buffer")
342 })?);
343 continue;
344 }
345 if let Some(x) = ArrayBuffer::from_object(object.clone()) {
346 data.extend_from_slice(unsafe { x.as_bytes() }.ok_or_else(|| {
349 Exception::throw_type(ctx, "Cannot create a blob with detached buffer")
350 })?);
351 continue;
352 }
353 }
354
355 let string = if elem.is_string() {
356 get_lossy_string(elem)?
357 } else {
358 Coerced::<String>::from_js(ctx, elem)?.0
359 };
360 if let EndingType::Transparent = endings {
361 data.extend_from_slice(string.as_bytes());
362 } else {
363 let len = string.len();
364 data.reserve(len);
365
366 let bytes = string.as_bytes();
367 let mut iter = bytes.iter();
368
369 let mut start = 0usize;
370 let mut i = 0usize;
371 let line_ending_is_n = LINE_ENDING[0] == b'\n';
372
373 while let Some(byte) = iter.next() {
374 if byte == &b'\r' {
375 if let Some(next_byte) = iter.next() {
376 data.extend(&bytes[start..i]);
377 i += 1;
378 start = i + 1;
379 if next_byte != &b'\n' {
380 data.extend([b'\r', *next_byte]);
381 } else {
382 data.extend(LINE_ENDING);
383 }
384 }
385 } else if byte == &b'\n' && !line_ending_is_n {
386 data.extend(&bytes[start..i]);
387 data.extend(LINE_ENDING);
388 start = i + 1;
389 };
390 i += 1;
391 }
392
393 if start < len {
394 data.extend(&bytes[start..len]);
395 }
396 }
397 }
398 Ok(data)
399}
400
401fn parse_endings<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Option<EndingType>> {
402 if value.is_undefined() {
403 return Ok(None);
404 }
405 let endings = match Coerced::<String>::from_js(ctx, value)?.0.as_str() {
406 "transparent" => Some(EndingType::Transparent),
407 "native" => Some(EndingType::Native),
408 _ => {
409 return Err(Exception::throw_type(
410 ctx,
411 r#"expected 'endings' to be either 'transparent' or 'native'"#,
412 ));
413 },
414 };
415 Ok(endings)
416}
417
418fn array_to_string(array: &Array) -> Result<String> {
419 let mut itoa_buffer = itoa::Buffer::new();
420 let mut ryu_buffer = ryu::Buffer::new();
421
422 let parts = array
423 .clone()
424 .into_iter()
425 .map(|value| {
426 let value = value?;
427 if let Some(string) = value.as_string() {
428 Ok(string.to_string()?)
429 } else if let Some(number) = value.as_int() {
430 Ok(itoa_buffer.format(number).to_string())
431 } else if let Some(number) = value.as_float() {
432 Ok(ryu_buffer.format(number).to_string())
433 } else {
434 Ok(String::new())
435 }
436 })
437 .collect::<Result<Vec<_>>>()?;
438
439 Ok(parts.join(","))
440}
441
442fn clamp_long_long(value: f64) -> isize {
443 if value.is_nan() {
444 return 0;
445 }
446 let rounded = value.round_ties_even();
447 rounded.clamp(isize::MIN as f64, isize::MAX as f64) as isize
448}