// SPDX-License-Identifier: MIT // Learning project: bounded reporting for ordinary local task files. import { open } from "node:fs/promises"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; export const MAX_BYTES = 65536; export const MAX_RECORDS = 1000; export function summarise(records) { if (!Array.isArray(records) || records.length > MAX_RECORDS) { throw new Error("Expected at most 1000 records"); } const counts = { closed: 0, open: 0 }; for (const record of records) { if (record === null || typeof record !== "object" || Array.isArray(record) || Object.keys(record).sort().join(",") !== "status,title") { throw new Error("Expected title and status only"); } const { title, status } = record; if (typeof title !== "string" || title.trim().length === 0 || [...title].length > 100) { throw new Error("Expected a nonblank title of at most 100 code points"); } if (status !== "open" && status !== "closed") { throw new Error("Expected open or closed status"); } counts[status] += 1; } return { by_status: counts, records: records.length }; } export async function readRecords(path) { const handle = await open(path, "r"); try { if (!(await handle.stat()).isFile()) throw new Error("Expected an ordinary file"); const buffer = Buffer.alloc(MAX_BYTES + 1); let length = 0; while (length < buffer.length) { const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null); if (bytesRead === 0) break; length += bytesRead; } if (length > MAX_BYTES) throw new Error("Input exceeds 65536 bytes"); const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }) .decode(buffer.subarray(0, length)); if (text.startsWith("\uFEFF")) throw new Error("BOM is not allowed"); return JSON.parse(text); } finally { await handle.close(); } } export async function main(args = process.argv.slice(2)) { if (args.length === 1 && args[0] === "--help") { console.log("Usage: node task_report.mjs INPUT.json"); return 0; } if (args.length !== 1) { console.error("error: expected one input file; use --help"); return 2; } let result; try { result = summarise(await readRecords(args[0])); } catch { console.error("error: invalid or unreadable task file"); return 2; } console.log(JSON.stringify(result)); return 0; } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { process.exitCode = await main(); }