All files multipart.js

100% Statements 55/55
100% Branches 23/23
100% Functions 3/3
100% Lines 55/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 568x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 220x 220x 8x 8x 8x 70x 70x 8x 8x 8x 8x 8x 8x 8x 8x 8x 73x 73x 148x 148x 147x 73x 73x 73x 68x 69x 69x 1x 1x 69x 72x 73x 147x 147x 147x 147x 147x 147x 147x 72x 72x 73x  
// Copyright 2026 优谷雅言 open.shengzhiai.com. Licensed under the Apache License, Version 2.0.
// multipart/form-data body built by hand as bytes (DESIGN 5.2). No FormData: a FormData Blob part
// always carries a filename, which turns the config part into a file part and breaks the signature.
 
import { concatBytes, indexOfBytes, randomBytes, toBytes, toHex, utf8 } from './util.js';
 
const CRLF = utf8('\r\n');
 
/** Content-Type of the native config part. */
export const CONFIG_PART_CONTENT_TYPE = 'application/json; charset=utf-8';
 
/** Escape a field name or filename the way browsers do for form-data. */
export function escapeFormName(name) {
  return String(name).replace(/"/g, '%22').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
}
 
/** Random boundary: ----YuguFormBoundary plus 24 hex characters. */
export function makeBoundary(cryptoImpl) {
  return `----YuguFormBoundary${toHex(randomBytes(12, cryptoImpl))}`;
}
 
/**
 * Build a multipart body.
 * parts: [{name, value}] for text fields, [{name, data, filename, contentType}] for files.
 * A part gets a filename only when filename is given, and a Content-Type line only when
 * contentType is given.
 * Returns {body: Uint8Array, contentType, boundary}.
 */
export function buildMultipart(parts, options) {
  const opts = options || {};
  const encoded = parts.map((p) => {
    const bytes = p.data !== undefined ? toBytes(p.data) : utf8(p.value == null ? '' : String(p.value));
    if (!bytes) throw new TypeError(`multipart part ${p.name} has unsupported data`);
    return { name: p.name, filename: p.filename, contentType: p.contentType, bytes };
  });
  let boundary = opts.boundary || makeBoundary(opts.crypto);
  if (!opts.boundary) {
    for (let tries = 0; tries < 8; tries++) {
      const marker = utf8(`--${boundary}`);
      if (!encoded.some((p) => indexOfBytes(p.bytes, marker) >= 0)) break;
      boundary = makeBoundary(opts.crypto);
    }
  }
  const chunks = [];
  for (const p of encoded) {
    let head = `--${boundary}\r\nContent-Disposition: form-data; name="${escapeFormName(p.name)}"`;
    if (p.filename != null) head += `; filename="${escapeFormName(p.filename)}"`;
    head += '\r\n';
    if (p.contentType) head += `Content-Type: ${p.contentType}\r\n`;
    head += '\r\n';
    chunks.push(utf8(head), p.bytes, CRLF);
  }
  chunks.push(utf8(`--${boundary}--\r\n`));
  return { body: concatBytes(chunks), contentType: `multipart/form-data; boundary=${boundary}`, boundary };
}