Skip to content

Vendored Pipenv re-serializes the whole Pipfile.lock, so a non-ASCII lock is rewritten throughout and its revert is not byte-identical #1128

Description

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

Kind: bug (a duplicated writer whose behavior has drifted, proven by execution). Source: review Part 5.4 ("Pipfile.lock is written two ways"); register E14.

Problem

Pipfile.lock has two writers:

  • Hosted mode (and upstream restore) splices single entries by byte span and leaves every other byte alone: redirect/pipenv.rs entries / format_entry.``
  • Vendored mode parses the lock into a serde_json::Value, edits it, and writes the whole document back through to_canonical_json.`` It does this on both wire (#L498) and revert (`#L686`).

to_canonical_json claims to be byte-identical to Pipenv's json.dumps(indent=4, sort_keys=True) "for the ASCII content pipenv locks carry". That holds only for ASCII. Pipenv's encoder uses Python's default ensure_ascii=True, so it writes any non-ASCII character as \uXXXX, while serde_json writes raw UTF-8. A Pipenv lock can carry non-ASCII text, for example a local path dependency in a directory named café, or a source name.

The writers have also drifted on a leading BOM. Hosted mode accepts one and keeps it (bom_prefixed_lock_is_rewritten_with_the_bom_intact).`` Vendored mode passes the raw text to serde_json::from_str and refuses it.

The Composer backend fixed this exact class of bug. vendor/composer_lock/lock_text.rs splices one entry and renders it in the lock's own slash and unicode escaping ([`escapes_unicode`](https://github.com/SocketDev/socket-patch/blob/e2d96336fbc449a87a20faae57afbed597e8788f/crates/socket-patch-core/src/vendor/composer_lock/lock_text.rs#L132-L160)),`` because "a whole-lock re-serialization rewrote what serde_json spells differently from the lock's writer". Pipenv never got the same fix.

A layering detail: vendored revert imports its relock test from the hosted engine (crate::patch::redirect::pipenv_reserialized_around_reference),`` which is a vendor → redirect edge.

Proof. A throwaway test in pypi_pipenv.rs's test module, run twice on e2d9633. It used the module's LOCK_DIRECT_REGISTRY fixture with one extra develop entry, "mylib": {"editable": true, "path": "./libs/café"}, written as Pipenv writes it:

Result (both runs)
vendored wire_pipenv the lock now contains raw café and no café: an unrelated entry was rewritten
vendored revert_pipenv after that wire success: true, but the lock is not byte-identical to the original (the escape stays lost)
hosted rewrite_registry_redirect on the same lock rewrites six and keeps café byte for byte, with no warnings
vendored load_pipenv_project on the fixture with a leading BOM pypi_pipenv_lock_parse_failed ("expected value at line 1 column 1"); hosted accepts the same file

So vendor --revert reports a clean revert while leaving a diff in Pipfile.lock, and vendor rewrites lines the user never asked it to touch. The test revert_round_trip_restores_lock_byte_identically passes only because every fixture is ASCII.

Symptoms

None filed. Related: #905 (BOM helper; its pending-files list doesn't name this missing strip) and #937 (the shared Poetry/PDM/Pipenv wire and revert envelope).

Impact: low severity, but it breaks a contract. Pipenv still reads the rewritten lock. The cost is a noisy diff on vendor, and a "byte-identical" revert that isn't one. The vendor --check and takeover drift checks compare parsed values, so they are not misled. Size: about 120 production lines change, and to_canonical_json and with_line_ending are deleted.

Proposed change

  1. Move the hosted span reader (entries, properties, format_entry) and reserialized_around_reference from patch/redirect/pipenv.rs into a new formats/pipenv.rs, as a pure move. Hosted, upstream restore and vendored all import it from there, which removes the vendor → redirect edge.
  2. Vendored wire and revert splice only the entries they change, through that module, as hosted does. Read the lock through formats::text::strip_bom and re-add the BOM on write.
  3. Delete to_canonical_json, with_line_ending and the pipenv_reserialized_around_reference re-export in redirect/mod.rs.
  4. Render a spliced entry in the lock's own escaping. Either share Composer's escapes_unicode / escape_non_ascii (moving them to formats::json or similar), or have format_entry emit \uXXXX whenever the surrounding text does.

Size and scope

Acceptance criteria

  • One Pipfile.lock entry reader/splicer, in formats/, used by hosted, upstream restore and vendored.
  • to_canonical_json and with_line_ending are deleted, and vendor/ no longer imports patch::redirect::pipenv_*.
  • Regression test: a lock carrying é in an unrelated entry. Vendored wire changes only the target entry's bytes, and wire + revert is byte-identical.
  • Regression test: a BOM-prefixed lock vendors and reverts with the BOM kept, matching hosted.
  • wiring_matches_fixtures_byte_identically, revert_round_trip_restores_lock_byte_identically and the redirect::pipenv tests stay green, along with cargo test -p socket-patch-core and the Pipenv CLI suites.

Dependencies


Backlog review — 2026-10-08

Priority: unassigned → P3. Keep the concrete Pipenv writer/BOM compatibility follow-up. Non-ASCII reserialization changes formatting while parsed values and installs remain correct; a BOM-prefixed file is refused safely. This is low-priority formatting and input compatibility, not P1 patch or attestation correctness.

Activity

  1. added
    bugSomething isn't working
    arch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)
    on Oct 8, 2026
  2. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue for the architecture refactor routine (highest leverage: deletes the second Pipfile.lock writer, fixes the non-ASCII and BOM drift, and all its files are free of open PRs). Branch: arch-refactor/1128-pipenv-one-splicer. Claim-ID: 2026-10-08T22:55:36Z-03a693


    Generated by Claude Code

  3. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1188.


    Generated by Claude Code

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:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)bugSomething isn't workingpm:pipenvPipenvpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions