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 5610x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 230x 230x 10x 10x 10x 73x 73x 10x 10x 10x 10x 10x 10x 10x 10x 10x 76x 76x 155x 155x 154x 76x 76x 76x 71x 72x 72x 1x 1x 72x 75x 76x 154x 154x 154x 154x 154x 154x 154x 75x 75x 76x  
// 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 };
}