rustploy 0.1.34

Rustploy PaaS daemon — lightweight self-hosted deployment platform
// zip.js — monta um `.zip` no navegador, sem biblioteca.
//
// Usado por "Baixar infra" (Settings → Sync): os arquivos que o daemon devolve em
// `InfraFiles` viram um zip sem compressão (são poucos arquivos de texto). O app
// desktop faz o mesmo com `zip_write` do motor.

const textEncoder = new TextEncoder();

const CRC_TABLE = (() => {
  const table = new Uint32Array(256);
  for (let n = 0; n < 256; n++) {
    let c = n;
    for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
    table[n] = c >>> 0;
  }
  return table;
})();

function crc32(bytes) {
  let crc = 0xffffffff;
  for (let i = 0; i < bytes.length; i++) crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
  return (crc ^ 0xffffffff) >>> 0;
}

/** Data/hora no formato DOS (o zip não guarda fuso). */
function dosDateTime(date) {
  const time = (date.getHours() << 11) | (date.getMinutes() << 5) | (date.getSeconds() >> 1);
  const day = ((date.getFullYear() - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
  return { time: time & 0xffff, day: day & 0xffff };
}

/** Monta o zip a partir de `[{ path, content }]` (texto), sem compressão, na ordem recebida. */
export function buildZip(files) {
  const { time, day } = dosDateTime(new Date());
  const entries = files.map((file) => ({
    name: textEncoder.encode(file.path),
    data: textEncoder.encode(file.content),
  }));

  const chunks = [];
  const central = [];
  let offset = 0;
  for (const entry of entries) {
    const crc = crc32(entry.data);
    const local = new DataView(new ArrayBuffer(30));
    local.setUint32(0, 0x04034b50, true);
    local.setUint16(4, 20, true); // versão mínima
    local.setUint16(6, 0x0800, true); // flag: nome em UTF-8
    local.setUint16(8, 0, true); // método: stored
    local.setUint16(10, time, true);
    local.setUint16(12, day, true);
    local.setUint32(14, crc, true);
    local.setUint32(18, entry.data.length, true);
    local.setUint32(22, entry.data.length, true);
    local.setUint16(26, entry.name.length, true);
    local.setUint16(28, 0, true);
    chunks.push(new Uint8Array(local.buffer), entry.name, entry.data);

    const head = new DataView(new ArrayBuffer(46));
    head.setUint32(0, 0x02014b50, true);
    head.setUint16(4, 20, true); // versão que criou
    head.setUint16(6, 20, true); // versão mínima
    head.setUint16(8, 0x0800, true);
    head.setUint16(10, 0, true);
    head.setUint16(12, time, true);
    head.setUint16(14, day, true);
    head.setUint32(16, crc, true);
    head.setUint32(20, entry.data.length, true);
    head.setUint32(24, entry.data.length, true);
    head.setUint16(28, entry.name.length, true);
    head.setUint32(42, offset, true);
    central.push(new Uint8Array(head.buffer), entry.name);

    offset += 30 + entry.name.length + entry.data.length;
  }

  const centralSize = central.reduce((total, part) => total + part.length, 0);
  const end = new DataView(new ArrayBuffer(22));
  end.setUint32(0, 0x06054b50, true);
  end.setUint16(8, entries.length, true);
  end.setUint16(10, entries.length, true);
  end.setUint32(12, centralSize, true);
  end.setUint32(16, offset, true);
  return new Blob([...chunks, ...central, new Uint8Array(end.buffer)], { type: "application/zip" });
}