549 lines
19 KiB
JavaScript
549 lines
19 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import {
|
|
copyFileSync,
|
|
cpSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync
|
|
} from "node:fs";
|
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = resolve(fileURLToPath(new URL("../../..", import.meta.url)));
|
|
const webRoot = join(root, "kdl-wasm", "web");
|
|
const deployRoot = join(webRoot, "test-results", "deploy");
|
|
const appRoot = join(webRoot, "app");
|
|
const specRoot = join(webRoot, "tests", "fixtures", "abb120", "spec-programs");
|
|
const resultRoot = join(webRoot, "test-results", "abb120-spec");
|
|
const uiResultRoot = join(webRoot, "test-results", "virtual-controller");
|
|
const working2Root = join(root, "working2");
|
|
const docsRoot = join(root, "work", "doc");
|
|
|
|
const jobId = getArg("--job") ?? latestJobId();
|
|
const generatedAt = new Date();
|
|
const releaseId = getArg("--release") ?? `${formatShanghaiId(generatedAt)}-${jobId}`;
|
|
const packageName = `kdl-olp-demo-${releaseId}`;
|
|
const packageDir = join(deployRoot, packageName);
|
|
const zipPath = join(deployRoot, `${packageName}.zip`);
|
|
|
|
const jobDir = join(resultRoot, jobId);
|
|
const manifest = readJson(join(specRoot, "manifest.json"));
|
|
const job = readJson(join(jobDir, "job.json"));
|
|
const report = readJson(join(jobDir, "report.json"));
|
|
const uiEvidence = readJson(join(uiResultRoot, "evidence.json"));
|
|
|
|
assertReportIsDeployable(report);
|
|
assertJobMatchesInputs(job, report, manifest);
|
|
assertSafeDeployPath(packageDir);
|
|
mkdirSync(deployRoot, { recursive: true });
|
|
rmSync(packageDir, { recursive: true, force: true });
|
|
mkdirSync(packageDir, { recursive: true });
|
|
|
|
copyTree(appRoot, join(packageDir, "app"));
|
|
copyTree(specRoot, join(packageDir, "spec-programs"));
|
|
copyTree(jobDir, join(packageDir, "test-results", "abb120-spec", jobId));
|
|
copyTree(uiResultRoot, join(packageDir, "test-results", "virtual-controller"));
|
|
copyDocs(join(packageDir, "docs"));
|
|
copyWorking2Docs(join(packageDir, "docs", "working2"));
|
|
|
|
const demoManifest = buildDemoManifest();
|
|
writeJson(join(packageDir, "demo-manifest.json"), demoManifest);
|
|
writeFileSync(join(packageDir, "index.html"), indexHtml(demoManifest), "utf8");
|
|
writeFileSync(join(packageDir, "README.md"), packageReadme(demoManifest), "utf8");
|
|
|
|
zipDirectory(packageDir, zipPath);
|
|
const zip = {
|
|
path: slash(relative(root, zipPath)),
|
|
bytes: statSync(zipPath).size,
|
|
sha256: sha256File(zipPath)
|
|
};
|
|
const summary = {
|
|
release_id: releaseId,
|
|
generated_at: generatedAt.toISOString(),
|
|
job_id: jobId,
|
|
package_dir: slash(relative(root, packageDir)),
|
|
zip,
|
|
program_count: demoManifest.program_count,
|
|
report_fail: report.summary.fail
|
|
};
|
|
writeJson(join(packageDir, "demo-build-summary.json"), summary);
|
|
writeJson(join(deployRoot, `${packageName}.summary.json`), summary);
|
|
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
|
|
function buildDemoManifest() {
|
|
const levelCounts = countLevels(manifest.programs);
|
|
const programs = manifest.programs.map((entry) => {
|
|
const jobProgram = job.programs.find((program) => program.program_id === entry.program_id);
|
|
return {
|
|
program_id: entry.program_id,
|
|
file: entry.file,
|
|
source: `spec-programs/${entry.file}`,
|
|
coverage_level: entry.coverage_level,
|
|
spec_sections: entry.spec_sections,
|
|
expected_status: entry.expected_status,
|
|
status: jobProgram?.status ?? "missing",
|
|
diagnostics: jobProgram?.diagnostics?.map((diagnostic) => diagnostic.code).filter(Boolean) ?? [],
|
|
artifacts: artifactLinks(entry.program_id)
|
|
};
|
|
});
|
|
|
|
return {
|
|
release_id: releaseId,
|
|
generated_at: generatedAt.toISOString(),
|
|
suite_id: manifest.suite_id,
|
|
job_id: job.job_id,
|
|
robot_id: manifest.robot_id,
|
|
spec_source: manifest.spec_source,
|
|
program_count: manifest.programs.length,
|
|
runtime_programs: levelCounts.Runtime,
|
|
static_programs: levelCounts.Static,
|
|
post_programs: levelCounts.Post,
|
|
entry: "app/virtual-controller.html",
|
|
virtual_controller_entry: "app/virtual-controller.html",
|
|
report_json: `test-results/abb120-spec/${job.job_id}/report.json`,
|
|
report_html: `test-results/abb120-spec/${job.job_id}/report.html`,
|
|
job_json: `test-results/abb120-spec/${job.job_id}/job.json`,
|
|
program_manifest: "spec-programs/manifest.json",
|
|
screenshots: {
|
|
desktop: "test-results/virtual-controller/virtual-controller-desktop.png",
|
|
mobile: "test-results/virtual-controller/virtual-controller-mobile.png",
|
|
evidence: "test-results/virtual-controller/evidence.json"
|
|
},
|
|
docs: [
|
|
{
|
|
title: "GRL user manual",
|
|
href: "docs/grl-user-manual.docx",
|
|
kind: "docx"
|
|
},
|
|
{
|
|
title: "OLP test document",
|
|
href: "docs/olp-test-document.docx",
|
|
kind: "docx"
|
|
},
|
|
{
|
|
title: "Working2 acceptance evidence",
|
|
href: "docs/working2/acceptance-evidence.md",
|
|
kind: "markdown"
|
|
}
|
|
],
|
|
coverage: report.coverage,
|
|
summary: report.summary,
|
|
programs
|
|
};
|
|
}
|
|
|
|
function artifactLinks(programId) {
|
|
const base = `test-results/abb120-spec/${job.job_id}/programs/${programId}`;
|
|
return {
|
|
compile: `${base}/compile.json`,
|
|
controller: `${base}/controller.json`,
|
|
motion_queue: `${base}/motion-queue.json`,
|
|
trace: `${base}/trace.json`,
|
|
io: `${base}/io.json`,
|
|
trajectory: `${base}/trajectory.json`,
|
|
post_report: `${base}/post-report.json`,
|
|
roundtrip: `${base}/roundtrip.json`
|
|
};
|
|
}
|
|
|
|
function copyDocs(targetDir) {
|
|
mkdirSync(targetDir, { recursive: true });
|
|
const docs = [
|
|
["GRL功能语法逻辑与程序创建使用手册.docx", "grl-user-manual.docx"],
|
|
["通用机器人离线编程系统测试文档.docx", "olp-test-document.docx"]
|
|
];
|
|
for (const [doc, alias] of docs) {
|
|
const source = join(docsRoot, doc);
|
|
if (!existsSync(source)) {
|
|
throw new Error(`Required document is missing: ${source}`);
|
|
}
|
|
copyFileSync(source, join(targetDir, doc));
|
|
copyFileSync(source, join(targetDir, alias));
|
|
}
|
|
}
|
|
|
|
function copyWorking2Docs(targetDir) {
|
|
mkdirSync(targetDir, { recursive: true });
|
|
for (const entry of readdirSync(working2Root, { withFileTypes: true })) {
|
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
copyFileSync(join(working2Root, entry.name), join(targetDir, entry.name));
|
|
}
|
|
}
|
|
const aliases = {
|
|
"01-项目功能内容.md": "project-scope.md",
|
|
"02-项目程序开发详细步骤.md": "development-steps.md",
|
|
"03-推进台账.md": "progress-log.md",
|
|
"04-任务矩阵.md": "task-matrix.md",
|
|
"05-验收证据.md": "acceptance-evidence.md",
|
|
"06-决策记录.md": "decisions.md",
|
|
"07-服务器发布与演示包实施方案.md": "server-deploy-plan.md"
|
|
};
|
|
for (const [sourceName, aliasName] of Object.entries(aliases)) {
|
|
const source = join(working2Root, sourceName);
|
|
if (existsSync(source)) {
|
|
copyFileSync(source, join(targetDir, aliasName));
|
|
}
|
|
}
|
|
const manifestDir = join(working2Root, "programs");
|
|
if (existsSync(manifestDir)) {
|
|
copyTree(manifestDir, join(targetDir, "programs"));
|
|
const manifestSource = join(manifestDir, "manifest.md");
|
|
if (existsSync(manifestSource)) {
|
|
copyFileSync(manifestSource, join(targetDir, "program-manifest.md"));
|
|
}
|
|
}
|
|
}
|
|
|
|
function indexHtml(demoManifest) {
|
|
const rows = demoManifest.programs.map((program) => `
|
|
<tr>
|
|
<td><a href="${escapeAttr(program.source)}">${escapeHtml(program.program_id)}</a></td>
|
|
<td>${escapeHtml(program.coverage_level)}</td>
|
|
<td>${escapeHtml(program.status)}</td>
|
|
<td>${escapeHtml(program.spec_sections.join(", "))}</td>
|
|
<td><a href="${escapeAttr(program.artifacts.compile)}">compile</a></td>
|
|
</tr>`).join("");
|
|
return `<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>KDL OLP Demo ${escapeHtml(demoManifest.release_id)}</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light;
|
|
--ink: #172026;
|
|
--muted: #5e6a71;
|
|
--line: #cfd7dc;
|
|
--panel: #f6f8f9;
|
|
--accent: #1f7a8c;
|
|
--accent-2: #7b6d3a;
|
|
font-family: Arial, "Microsoft YaHei", sans-serif;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; color: var(--ink); background: #ffffff; }
|
|
header { padding: 28px 32px 18px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
h1 { margin: 0 0 10px; font-size: 28px; line-height: 1.2; letter-spacing: 0; }
|
|
p { margin: 0; color: var(--muted); line-height: 1.55; }
|
|
main { max-width: 1180px; margin: 0 auto; padding: 24px 24px 40px; }
|
|
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 18px 0 24px; }
|
|
.actions a { color: #fff; background: var(--accent); text-decoration: none; padding: 10px 13px; border-radius: 6px; font-weight: 700; }
|
|
.actions a.secondary { background: var(--accent-2); }
|
|
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 24px; }
|
|
.summary div { border: 1px solid var(--line); border-radius: 6px; padding: 12px; background: #fff; }
|
|
.summary strong { display: block; font-size: 22px; }
|
|
section { margin-top: 28px; }
|
|
h2 { font-size: 18px; margin: 0 0 12px; }
|
|
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
th, td { border: 1px solid var(--line); padding: 9px 10px; text-align: left; vertical-align: top; }
|
|
th { background: var(--panel); }
|
|
img { max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 6px; }
|
|
.screens { display: grid; grid-template-columns: minmax(0, 2fr) minmax(220px, 1fr); gap: 16px; align-items: start; }
|
|
@media (max-width: 760px) {
|
|
header { padding: 22px 18px 14px; }
|
|
main { padding: 18px 14px 32px; }
|
|
h1 { font-size: 23px; }
|
|
.screens { grid-template-columns: 1fr; }
|
|
table { font-size: 13px; }
|
|
th, td { padding: 7px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>KDL OLP ABB120 Working2 Demo</h1>
|
|
<p>Release ${escapeHtml(demoManifest.release_id)} / Job ${escapeHtml(demoManifest.job_id)} / Suite ${escapeHtml(demoManifest.suite_id)}</p>
|
|
</header>
|
|
<main>
|
|
<nav class="actions" aria-label="demo links">
|
|
<a href="${escapeAttr(demoManifest.virtual_controller_entry)}">Open virtual controller</a>
|
|
<a href="${escapeAttr(demoManifest.report_html)}" class="secondary">Open report</a>
|
|
<a href="${escapeAttr(demoManifest.program_manifest)}" class="secondary">Program manifest</a>
|
|
<a href="demo-manifest.json" class="secondary">Demo manifest</a>
|
|
</nav>
|
|
<div class="summary">
|
|
<div><span>Programs</span><strong>${demoManifest.program_count}</strong></div>
|
|
<div><span>Runtime</span><strong>${demoManifest.runtime_programs}</strong></div>
|
|
<div><span>Static</span><strong>${demoManifest.static_programs}</strong></div>
|
|
<div><span>Post</span><strong>${demoManifest.post_programs}</strong></div>
|
|
<div><span>Fail</span><strong>${demoManifest.summary.fail}</strong></div>
|
|
</div>
|
|
<section>
|
|
<h2>Program Evidence</h2>
|
|
<table>
|
|
<thead><tr><th>Program</th><th>Layer</th><th>Status</th><th>Spec Sections</th><th>Artifact</th></tr></thead>
|
|
<tbody>${rows}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
<section>
|
|
<h2>Screenshots</h2>
|
|
<div class="screens">
|
|
<a href="${escapeAttr(demoManifest.screenshots.desktop)}"><img src="${escapeAttr(demoManifest.screenshots.desktop)}" alt="Virtual controller desktop screenshot"></a>
|
|
<a href="${escapeAttr(demoManifest.screenshots.mobile)}"><img src="${escapeAttr(demoManifest.screenshots.mobile)}" alt="Virtual controller mobile screenshot"></a>
|
|
</div>
|
|
</section>
|
|
<section>
|
|
<h2>Documents</h2>
|
|
<table>
|
|
<tbody>
|
|
${demoManifest.docs.map((doc) => `<tr><td>${escapeHtml(doc.title)}</td><td><a href="${escapeAttr(doc.href)}">${escapeHtml(doc.href)}</a></td></tr>`).join("\n ")}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
function packageReadme(demoManifest) {
|
|
return [
|
|
`# KDL OLP demo ${demoManifest.release_id}`,
|
|
"",
|
|
`Job: ${demoManifest.job_id}`,
|
|
`Suite: ${demoManifest.suite_id}`,
|
|
`Programs: ${demoManifest.program_count}`,
|
|
`Report: ${demoManifest.report_html}`,
|
|
`Virtual controller: ${demoManifest.virtual_controller_entry}`,
|
|
"",
|
|
"Serve this directory as the HTTPS document root for the demo port."
|
|
].join("\n");
|
|
}
|
|
|
|
function latestJobId() {
|
|
if (!existsSync(resultRoot)) {
|
|
throw new Error(`No ABB120 spec result directory found: ${resultRoot}`);
|
|
}
|
|
const jobs = readdirSync(resultRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory() && entry.name.startsWith("W2-JOB-"))
|
|
.map((entry) => ({
|
|
name: entry.name,
|
|
mtimeMs: statSync(join(resultRoot, entry.name)).mtimeMs
|
|
}))
|
|
.sort((left, right) => right.mtimeMs - left.mtimeMs || right.name.localeCompare(left.name));
|
|
if (jobs.length === 0) {
|
|
throw new Error(`No W2-JOB-* result directories found in ${resultRoot}`);
|
|
}
|
|
return jobs[0].name;
|
|
}
|
|
|
|
function assertReportIsDeployable(report) {
|
|
if (report.summary?.programs !== 19) {
|
|
throw new Error(`Expected report.summary.programs=19, got ${report.summary?.programs}`);
|
|
}
|
|
if (report.summary?.fail !== 0) {
|
|
throw new Error(`Expected report.summary.fail=0, got ${report.summary?.fail}`);
|
|
}
|
|
if (report.coverage?.missingSections?.length !== 0) {
|
|
throw new Error(`Expected no missing sections, got ${report.coverage.missingSections.join(",")}`);
|
|
}
|
|
}
|
|
|
|
function assertJobMatchesInputs(job, report, manifest) {
|
|
if (job.job_id !== jobId || report.job_id !== jobId) {
|
|
throw new Error(`Job id mismatch: requested ${jobId}, job=${job.job_id}, report=${report.job_id}`);
|
|
}
|
|
if (job.suite_id !== manifest.suite_id || report.suite_id !== manifest.suite_id) {
|
|
throw new Error("Suite id mismatch between manifest, job, and report");
|
|
}
|
|
if (manifest.programs.length !== 19 || job.programs.length !== 19) {
|
|
throw new Error(`Expected 19 programs, manifest=${manifest.programs.length}, job=${job.programs.length}`);
|
|
}
|
|
}
|
|
|
|
function countLevels(programs) {
|
|
return programs.reduce((counts, program) => {
|
|
counts[program.coverage_level] = (counts[program.coverage_level] ?? 0) + 1;
|
|
return counts;
|
|
}, { Runtime: 0, Static: 0, Post: 0 });
|
|
}
|
|
|
|
function copyTree(source, target) {
|
|
if (!existsSync(source)) {
|
|
throw new Error(`Required path is missing: ${source}`);
|
|
}
|
|
mkdirSync(dirname(target), { recursive: true });
|
|
cpSync(source, target, { recursive: true, force: true });
|
|
}
|
|
|
|
function readJson(file) {
|
|
return JSON.parse(readFileSync(file, "utf8"));
|
|
}
|
|
|
|
function writeJson(file, value) {
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
}
|
|
|
|
function zipDirectory(sourceDir, destinationFile) {
|
|
const files = walkFiles(sourceDir).sort((left, right) => left.localeCompare(right));
|
|
const chunks = [];
|
|
const central = [];
|
|
let offset = 0;
|
|
|
|
for (const file of files) {
|
|
const data = readFileSync(file);
|
|
const name = slash(relative(sourceDir, file));
|
|
const nameBuffer = Buffer.from(name, "utf8");
|
|
const stat = statSync(file);
|
|
const { time, date } = dosDateTime(stat.mtime);
|
|
const crc = crc32(data);
|
|
const local = Buffer.alloc(30);
|
|
local.writeUInt32LE(0x04034b50, 0);
|
|
local.writeUInt16LE(20, 4);
|
|
local.writeUInt16LE(0x0800, 6);
|
|
local.writeUInt16LE(0, 8);
|
|
local.writeUInt16LE(time, 10);
|
|
local.writeUInt16LE(date, 12);
|
|
local.writeUInt32LE(crc, 14);
|
|
local.writeUInt32LE(data.length, 18);
|
|
local.writeUInt32LE(data.length, 22);
|
|
local.writeUInt16LE(nameBuffer.length, 26);
|
|
local.writeUInt16LE(0, 28);
|
|
chunks.push(local, nameBuffer, data);
|
|
|
|
const header = Buffer.alloc(46);
|
|
header.writeUInt32LE(0x02014b50, 0);
|
|
header.writeUInt16LE(20, 4);
|
|
header.writeUInt16LE(20, 6);
|
|
header.writeUInt16LE(0x0800, 8);
|
|
header.writeUInt16LE(0, 10);
|
|
header.writeUInt16LE(time, 12);
|
|
header.writeUInt16LE(date, 14);
|
|
header.writeUInt32LE(crc, 16);
|
|
header.writeUInt32LE(data.length, 20);
|
|
header.writeUInt32LE(data.length, 24);
|
|
header.writeUInt16LE(nameBuffer.length, 28);
|
|
header.writeUInt16LE(0, 30);
|
|
header.writeUInt16LE(0, 32);
|
|
header.writeUInt16LE(0, 34);
|
|
header.writeUInt16LE(0, 36);
|
|
header.writeUInt32LE(0, 38);
|
|
header.writeUInt32LE(offset, 42);
|
|
central.push(header, nameBuffer);
|
|
offset += local.length + nameBuffer.length + data.length;
|
|
}
|
|
|
|
const centralOffset = offset;
|
|
const centralSize = central.reduce((total, chunk) => total + chunk.length, 0);
|
|
const end = Buffer.alloc(22);
|
|
end.writeUInt32LE(0x06054b50, 0);
|
|
end.writeUInt16LE(0, 4);
|
|
end.writeUInt16LE(0, 6);
|
|
end.writeUInt16LE(files.length, 8);
|
|
end.writeUInt16LE(files.length, 10);
|
|
end.writeUInt32LE(centralSize, 12);
|
|
end.writeUInt32LE(centralOffset, 16);
|
|
end.writeUInt16LE(0, 20);
|
|
|
|
rmSync(destinationFile, { force: true });
|
|
writeFileSync(destinationFile, Buffer.concat([...chunks, ...central, end]));
|
|
}
|
|
|
|
function sha256File(file) {
|
|
return createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
}
|
|
|
|
function formatShanghaiId(date) {
|
|
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
timeZone: "Asia/Shanghai",
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
hour12: false
|
|
}).formatToParts(date);
|
|
const value = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
return `${value.year}${value.month}${value.day}-${value.hour}${value.minute}${value.second}`;
|
|
}
|
|
|
|
function getArg(name) {
|
|
const index = process.argv.indexOf(name);
|
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
}
|
|
|
|
function slash(value) {
|
|
return value.split(sep).join("/");
|
|
}
|
|
|
|
function psSingleQuoted(value) {
|
|
return `'${String(value).replaceAll("'", "''")}'`;
|
|
}
|
|
|
|
function assertSafeDeployPath(path) {
|
|
const allowedRoot = resolve(deployRoot) + sep;
|
|
const resolved = resolve(path);
|
|
if (!resolved.startsWith(allowedRoot)) {
|
|
throw new Error(`Refusing to modify path outside deploy root: ${resolved}`);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
}
|
|
|
|
function escapeAttr(value) {
|
|
return escapeHtml(value);
|
|
}
|
|
|
|
function walkFiles(dir) {
|
|
const files = [];
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const fullPath = join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...walkFiles(fullPath));
|
|
} else if (entry.isFile()) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function dosDateTime(date) {
|
|
const year = Math.max(1980, date.getFullYear());
|
|
const month = date.getMonth() + 1;
|
|
const day = date.getDate();
|
|
const hours = date.getHours();
|
|
const minutes = date.getMinutes();
|
|
const seconds = Math.floor(date.getSeconds() / 2);
|
|
return {
|
|
time: (hours << 11) | (minutes << 5) | seconds,
|
|
date: ((year - 1980) << 9) | (month << 5) | day
|
|
};
|
|
}
|
|
|
|
var crcTable;
|
|
|
|
function crc32(buffer) {
|
|
crcTable ??= createCrcTable();
|
|
let crc = 0xffffffff;
|
|
for (const byte of buffer) {
|
|
crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
}
|
|
return (crc ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function createCrcTable() {
|
|
const table = new Uint32Array(256);
|
|
for (let index = 0; index < 256; index += 1) {
|
|
let value = index;
|
|
for (let bit = 0; bit < 8; bit += 1) {
|
|
value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1);
|
|
}
|
|
table[index] = value >>> 0;
|
|
}
|
|
return table;
|
|
}
|