@prodcycle/prodcycle 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/utils/fs.js +31 -12
  2. package/package.json +1 -1
package/dist/utils/fs.js CHANGED
@@ -38,6 +38,12 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const minimatch_1 = require("minimatch");
40
40
  const MAX_FILE_SIZE = 256 * 1024; // 256 KB
41
+ // Reusable strict UTF-8 decoder. `TextDecoder` is stateless on the same
42
+ // instance (encoding + fatal are fixed at construction), so we allocate
43
+ // once at module load instead of once per file in the walk loop —
44
+ // repos with thousands of files would otherwise produce thousands of
45
+ // short-lived decoder objects per scan.
46
+ const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
41
47
  /**
42
48
  * Total file ceiling per scan. Hit on the OSS-CLI benchmark scanning
43
49
  * `hapifhir/hapi-fhir` (~13k files) — the CLI silently dropped ~3k files
@@ -397,18 +403,31 @@ function walk(dir, repoRoot, gitignores, prodcycleIgnores, includePatterns, user
397
403
  }
398
404
  if (isBinary(buffer))
399
405
  continue;
400
- const content = buffer.toString('utf8');
401
- // Post-decode filter: the service enforces the 256 KB per-file limit
402
- // on the UTF-8 byte length of the decoded string content, which can
403
- // differ from the file's on-disk byte count. `buffer.toString('utf8')`
404
- // silently replaces invalid UTF-8 byte sequences with U+FFFD (3 UTF-8
405
- // bytes each), so a file with invalid bytes that's under 256 KB on
406
- // disk can balloon over the limit after the round trip. The cheap
407
- // `stats.size` check above would let it through; the service then
408
- // rejects the entire chunk with 413 and torpedoes the scan.
409
- // Re-measuring here keeps the CLI's filter aligned with the service
410
- // enforcement. Concrete case from the GA-validation sweep:
411
- // `web/pnpm-lock.yaml` with stray non-UTF-8 bytes.
406
+ // Strict UTF-8 decode: throw (and skip the file) on invalid byte
407
+ // sequences. Pre-fix this was `buffer.toString('utf8')`, which
408
+ // silently replaces invalid bytes with U+FFFD and includes the file
409
+ // anyway. Python's `open(encoding='utf-8')` raises UnicodeDecodeError
410
+ // on the same input and skips the file via its except clause, so
411
+ // Node ended up sending files Python wouldn't. Those files were
412
+ // overwhelmingly garbage (U+FFFD soup with no real content), but
413
+ // the inflated payload pushed many scans over the sync /validate
414
+ // limit and into the chunked-session fallback Node ran 5–75x
415
+ // slower than Python on the same repos (dexidp-dex: npm=525s vs
416
+ // py=7s, frappe-erpnext: 1448s vs 42s, both 0 finding differences)
417
+ // during the 2026-05-12 GA-validation sweep. Catch the decode
418
+ // error and treat exactly like Python.
419
+ let content;
420
+ try {
421
+ content = UTF8_DECODER.decode(buffer);
422
+ }
423
+ catch {
424
+ continue;
425
+ }
426
+ // Post-decode size filter mirrors the service's 256 KB per-file
427
+ // enforcement. Without invalid-UTF-8 inflation (now skipped above),
428
+ // post-decode byte length usually matches `stats.size`, but a BOM
429
+ // or rare normalization edge case can still differ — keep the
430
+ // re-measure as defense-in-depth.
412
431
  if (Buffer.byteLength(content, 'utf8') > MAX_FILE_SIZE)
413
432
  continue;
414
433
  files[relPath] = content;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prodcycle/prodcycle",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Multi-framework policy-as-code compliance scanner for infrastructure and application code.",
5
5
  "homepage": "https://docs.prodcycle.com",
6
6
  "repository": {