// Installs the pinned s3d release into Pithos's own bin folder. The digests are // GitHub's published sha256 for each release asset; a download that does not // match is discarded before anything is written to the bin folder. import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import zlib from 'node:zlib'; export const S3D_VERSION = '0.2.0'; const ASSETS = { 'win32-x64': { file: 's3d_windows_amd64.zip', sha256: '239356ef55228d03aa1889c7208357b83faf66947ade2cba4f7a3893c632c473' }, 'linux-x64': { file: 's3d_linux_amd64.zip', sha256: 'e537d68be8b6749e3e239d572c497a4f1ebc81ef5a3b70d5ec3cc9ffa6498e73' }, 'linux-arm64': { file: 's3d_linux_arm64.zip', sha256: '76848764f09922f31489c809514cdc928b3882836d6e5b64dcac085a84b4958b' }, 'darwin-x64': { file: 's3d_darwin_amd64.zip', sha256: 'd7fe282efab35242b001ed9040d833334034682225b4c2b6b2eda67ecca8992e' }, 'darwin-arm64': { file: 's3d_darwin_arm64.zip', sha256: '9eb6b73cb30b5b6b54fbbc583af6f7d9e56a30c55247ac70568918a1c98a3353' }, }; export function assetForPlatform() { return ASSETS[`${process.platform}-${process.arch}`] || null; } export async function installS3d(binDir, onProgress = () => {}) { const asset = assetForPlatform(); if (!asset) throw new Error(`no s3d release for ${process.platform}-${process.arch}`); const url = `https://github.com/SiaFoundation/s3d/releases/download/v${S3D_VERSION}/${asset.file}`; onProgress({ phase: 'download', url }); const res = await fetch(url, { redirect: 'follow' }); if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`); const zip = Buffer.from(await res.arrayBuffer()); const digest = crypto.createHash('sha256').update(zip).digest('hex'); if (digest !== asset.sha256) throw new Error(`checksum mismatch for ${asset.file} (got ${digest})`); onProgress({ phase: 'extract' }); const exe = process.platform === 'win32' ? 's3d.exe' : 's3d'; const data = extractFromZip(zip, (name) => path.basename(name) === exe); if (!data) throw new Error(`${exe} not found in ${asset.file}`); fs.mkdirSync(binDir, { recursive: true }); const target = path.join(binDir, exe); const tmp = target + '.new'; fs.writeFileSync(tmp, data, { mode: 0o755 }); fs.renameSync(tmp, target); onProgress({ phase: 'done', path: target }); return target; } // Minimal zip reader: walks the central directory and inflates one entry. function extractFromZip(buf, match) { let eocd = -1; for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) { if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; } } if (eocd < 0) throw new Error('not a zip file'); const count = buf.readUInt16LE(eocd + 10); let p = buf.readUInt32LE(eocd + 16); for (let n = 0; n < count; n++) { if (buf.readUInt32LE(p) !== 0x02014b50) throw new Error('corrupt zip directory'); const method = buf.readUInt16LE(p + 10); const csize = buf.readUInt32LE(p + 20); const nameLen = buf.readUInt16LE(p + 28); const extraLen = buf.readUInt16LE(p + 30); const commentLen = buf.readUInt16LE(p + 32); const local = buf.readUInt32LE(p + 42); const name = buf.toString('utf8', p + 46, p + 46 + nameLen); p += 46 + nameLen + extraLen + commentLen; if (!match(name)) continue; const lName = buf.readUInt16LE(local + 26); const lExtra = buf.readUInt16LE(local + 28); const start = local + 30 + lName + lExtra; const raw = buf.subarray(start, start + csize); if (method === 0) return Buffer.from(raw); if (method === 8) return zlib.inflateRawSync(raw); throw new Error(`unsupported zip method ${method}`); } return null; }