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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions