Skip to content

pnpm lock and workspace readers don't skip a leading BOM, because BOM handling has no shared helper (4 named copies, ~50 inline strips) #905

Description

[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: discussion #560 register.

Kind: bug (with a refactor fix). Source: new finding; review "CRLF/BOM/indent policy" (Part 4.4), register E64.

Problem

No module owns "a leading UTF-8 BOM is encoding, not content". Each reader decides for itself, on main @ 9c43dfc:

Proof by execution. I ran a throwaway unit test twice on 9c43dfc with identical results. It used one pnpm 9 lock with one left-pad@1.3.0 entry, once plain and once with a \u{feff} prefix:

reader (consumer) plain BOM
PnpmLock::entries (inventory, hosted rewrite) 1 entry 1 entry
inventory_project_diagnosed 1 entry 1 entry
PnpmLock::is_pnpm_lock (VEX discovery) true false: "not a pnpm lockfile" diag
sniff_lock_grammar / detect_npm_lock_flavor (vendored router) V9 / Pnpm Err vendor_lockfile_version_unsupported: "has no lockfileVersion in its head … re-lock with pnpm >= 9"
lock_version_major (hosted trust gate) 9 None
workspace::top_level_key("trustLockfile: false") trustLockfile \u{feff}trustLockfile
workspace_lockfile_dir("lockfileDir: ../x") ../x ../x
formats::yarn::is_berry_lock (control) true true

So a single BOM lock gets four answers inside formats::pnpm: it is readable, not a pnpm lock, unversioned, and unsupported. pnpm itself reads it (see #903).

Symptoms

Impact: medium. Each new reader repeats the decision, and every one that forgets it is a new bug. Three bug-hunt issues have hit this in different ecosystems so far.

Proposed change

  1. Add formats::text with split_bom(&str) -> (&str, &str) (one BOM, as parse_json_manifest pins down) and strip_bom. Delete utils::serde::strip_bom, gradle::dsl::strip_bom, formats::yarn::strip_bom and cargo_manifest::split_bom, and re-point their callers.
  2. In formats::pnpm: have head_lock_version, lock_versions, is_pnpm_lock_text and may_need_store_flag read strip_bom(text). Have the pnpm-workspace.yaml splices call top_level_key on a BOM-stripped first line, re-adding the BOM on write. Then make governing_root::workspace_lockfile_dir a top_level_key caller, which deletes its private key-prefix grammar.
  3. Re-point the inline strip_prefix('\u{feff}') / trim_start_matches('\u{feff}') sites to the helper, file by file. Any site that keeps "any number of BOMs" must justify it in a comment.

Size and scope

Acceptance criteria

Dependencies

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:claimedagent:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)bugSomething isn't workingpm:pnpmpnpmpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions