You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/02 16:37:14 UTC

(maka) branch test/windows-repro-probe updated: test(release): isolate archive and NSIS header timestamps

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch test/windows-repro-probe
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/test/windows-repro-probe by this push:
     new 1183b9780c test(release): isolate archive and NSIS header timestamps
1183b9780c is described below

commit 1183b9780c7ec8af9fb2af1f7d1f71bd9119d870
Author: AstroHan <le...@outlook.com>
AuthorDate: Thu Sep 3 00:37:07 2026 +0800

    test(release): isolate archive and NSIS header timestamps
    
    Generated-by: Codex
---
 scripts/diagnose-windows-archives.mjs | 84 ++++++++++++++++++++++++++++++++++-
 1 file changed, 82 insertions(+), 2 deletions(-)

diff --git a/scripts/diagnose-windows-archives.mjs b/scripts/diagnose-windows-archives.mjs
index 8705f17e2d..19b84f0c27 100644
--- a/scripts/diagnose-windows-archives.mjs
+++ b/scripts/diagnose-windows-archives.mjs
@@ -20,9 +20,10 @@
 import assert from 'node:assert/strict';
 import { execFileSync } from 'node:child_process';
 import { createHash } from 'node:crypto';
-import { mkdtempSync, readFileSync, readdirSync } from 'node:fs';
+import { mkdtempSync, readFileSync, readdirSync, statSync } from 'node:fs';
 import { tmpdir } from 'node:os';
 import { join, resolve } from 'node:path';
+import { inflateRawSync } from 'node:zlib';
 import { comparePe } from './diagnose-windows-repro.mjs';
 
 const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
@@ -49,6 +50,22 @@ function artifact(sample, name) {
 
 function zipEvidence(bytes) {
   const normalized = Buffer.from(bytes);
+  function isolateNtfsTimes(offset, size) {
+    const end = offset + size;
+    while (offset < end) {
+      const id = bytes.readUInt16LE(offset);
+      const length = bytes.readUInt16LE(offset + 2);
+      assert.ok(offset + 4 + length <= end);
+      if (id === 10) {
+        assert.equal(length, 32);
+        assert.equal(bytes.readUInt16LE(offset + 8), 1);
+        assert.equal(bytes.readUInt16LE(offset + 10), 24);
+        normalized.fill(0, offset + 12, offset + 36);
+      }
+      offset += 4 + length;
+    }
+    assert.equal(offset, end);
+  }
   let end = bytes.length - 22;
   while (end >= Math.max(0, bytes.length - 65557) && bytes.readUInt32LE(end) !== 0x06054b50) end--;
   assert.ok(end >= 0, 'Missing ZIP end record');
@@ -80,6 +97,8 @@ function zipEvidence(bytes) {
     });
     normalized.fill(0, offset + 12, offset + 16);
     normalized.fill(0, local + 10, local + 14);
+    isolateNtfsTimes(offset + 46 + nameLength, extraLength);
+    isolateNtfsTimes(local + 30 + bytes.readUInt16LE(local + 26), bytes.readUInt16LE(local + 28));
     offset += 46 + nameLength + extraLength + commentLength;
   }
   assert.equal(offset, end, 'Unexpected bytes between central directory and end record');
@@ -122,6 +141,51 @@ const extractedPe = extractedDifferences
     name,
     comparison: comparePe(...extracted.map((sample) => readFileSync(join(sample.root, name)))),
   }));
+
+const nsisHeaders = samples.map((sample, index) => {
+  const bytes = artifact(sample, exeName);
+  const marker = bytes.indexOf(Buffer.from('efbeadde4e756c6c736f6674496e7374', 'hex'));
+  assert.ok(marker >= 4, 'NSIS first header missing');
+  const first = marker - 4;
+  const length = bytes.readUInt32LE(first + 28);
+  assert.ok(length >>> 31, 'Expected a compressed NSIS header');
+  const dataStart = first + 32 + (length & 0x7fffffff);
+  const header = inflateRawSync(bytes.subarray(first + 32, dataStart));
+  assert.equal(header.length, bytes.readUInt32LE(first + 20));
+  const normalized = Buffer.from(header);
+  const times = [...extracted[index].files.keys()].map((name) => {
+    const mtime = statSync(join(extracted[index].root, name), { bigint: true }).mtimeNs;
+    const time = Buffer.alloc(8);
+    time.writeBigUInt64LE(mtime / 100n + 116444736000000000n);
+    const offsets = [];
+    for (
+      let offset = header.indexOf(time);
+      offset >= 0;
+      offset = header.indexOf(time, offset + 8)
+    ) {
+      normalized.fill(0, offset, offset + 8);
+      offsets.push(offset);
+    }
+    return { name, filetime: time.toString('hex'), offsets };
+  });
+  return { header, normalized, times, dataHash: hash(bytes.subarray(dataStart, -4)) };
+});
+const headerDifferences = [];
+for (
+  let offset = 0;
+  offset < Math.max(...nsisHeaders.map((entry) => entry.header.length));
+  offset++
+) {
+  if (nsisHeaders[0].header[offset] !== nsisHeaders[1].header[offset]) {
+    if (headerDifferences.length < 24)
+      headerDifferences.push({
+        offset,
+        bytes: nsisHeaders.map((entry) =>
+          entry.header.subarray(offset, offset + 8).toString('hex'),
+        ),
+      });
+  }
+}
 console.log(
   JSON.stringify(
     {
@@ -130,7 +194,14 @@ console.log(
       verifiedArchiveHashes: true,
       zip: {
         entryCounts: zips.map((zip) => zip.entries.length),
-        equalAfterOnlyDosTimeIsolation: zips[0].normalizedHash === zips[1].normalizedHash,
+        equalAfterDosAndNtfsTimeIsolation: zips[0].normalizedHash === zips[1].normalizedHash,
+        compressedEntryDataEqual:
+          zips[0].entries.length === zips[1].entries.length &&
+          zips[0].entries.every(
+            (entry, index) =>
+              entry.name === zips[1].entries[index].name &&
+              entry.compressedHash === zips[1].entries[index].compressedHash,
+          ),
         changedEntries: zipChanges.length,
         firstChanges: zipChanges.slice(0, 12),
       },
@@ -138,6 +209,15 @@ console.log(
       extractedFileCounts: extracted.map((sample) => sample.files.size),
       extractedDifferences,
       extractedPe,
+      nsisHeader: {
+        sizes: nsisHeaders.map((entry) => entry.header.length),
+        dataAfterHeaderEqual: nsisHeaders[0].dataHash === nsisHeaders[1].dataHash,
+        equalAfterExtractedFiletimeIsolation: nsisHeaders[0].normalized.equals(
+          nsisHeaders[1].normalized,
+        ),
+        times: nsisHeaders.map((entry) => entry.times),
+        firstDifferences: headerDifferences,
+      },
       note: 'Only in-memory diagnostic isolation; original artifacts are not modified or executed. Archive extraction alone does not establish installer equivalence.',
     },
     null,


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org