atuinsh/atuin · info

fixed-layout structure cannot fail serialization

Error message

fixed-layout structure cannot fail serialization

What it means

Panic while serializing PackIA to JSON for a PASETO v4 implicit assertion. serde_json::to_string fails only for types that cannot be represented (non-string map keys, unhandled fallible Serialize impls); PackIA is a fixed layout of UUID-backed newtypes (RecordId, RecordIdx, HostId), a &str version, and a RecordTag — all infallible serializers with string-map-free output, hence the message 'fixed-layout structure cannot fail serialization'.

Source

Thrown at crates/atuin-client/src/packfile/record.rs:261

/// An implicit assertion that matches this [`PackManifestRecordView`].
///
/// Do *not* modify this struct. Just like `atuin_domain::record::AdditionalData`, it gets
/// serialized to JSON during encryption, and we rely on the serialization staying the same across
/// versions. Field order, types, and even names all must stay the same!
#[derive(Debug, Serialize)]
struct PackIA<'a> {
    pub manifest_id: RecordId,
    pub manifest_idx: RecordIdx,
    pub manifest_version: &'a str,
    pub host: HostId,
    pub tag: &'a RecordTag,
}

impl PackIA<'_> {
    /// The JSON an [`paseto_v4::ImplicitAssertion`] is built from.
    fn json(&self) -> String {
        serde_json::to_string(self).expect("fixed-layout structure cannot fail serialization")
    }
}

impl<'a> PackManifestRecordView<'a> {
    /// Decided on `12` because that's what Claude's experiments showed to be the good trade-off
    /// between compression size and compression speed and would be optimal for DSL/Fiber networks.
    const ZSTD_ENCODING_LEVEL: NonZeroU8 = NonZeroU8::new(12).unwrap();

    pub fn new(record: &'a Record<EncryptedData>) -> Result<Self, ParsingError> {
        let manifest = PackManifestData::parse(record)?;
        Ok(Self { record, manifest })
    }

    /// The range of history this manifest covers. Validated when the view was built.
    #[must_use]
    pub const fn range(&self) -> std::ops::Range<RecordIdx> {
        self.manifest.range()
    }

View on GitHub (pinned to 15fe1318f1)

Solutions

  1. Treat a crash here as a code regression, not an environment problem: inspect recent changes to PackIA's fields
  2. When adding fields to PackIA, keep them to strings/UUID newtypes/numbers, or switch this call to real error handling with map_err
  3. If a fallible type is genuinely needed, replace expect with `.map_err(|e| eyre!("implicit assertion serialization failed: {e}"))?` and propagate
Defensive patterns

Strategy: fallback

Try / catch

// Packfile encryption paths: treat a serialization panic as a hard code bug
let packed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    view.ia_json() // example wrapper around PackIA::json
}));
let json = packed.unwrap_or_else(|_| {
    tracing::error!("PackIA serialization invariant broken — packfile format changed?");
    // abort the pack operation; do not write a malformed pack
    std::process::abort();
});

Prevention

When it happens

Trigger: Calling PackManifestRecordView assertion-building code during packfile encryption (building the implicit-assertion JSON for a manifest record). The only theoretical path to failure is a future field being added whose Serialize impl returns an error (e.g., a map with non-string keys or a custom fallible type).

Common situations: None on current code. It becomes reachable if someone extends PackIA with a type like a HashMap<NonStringKey, _> or a Serialize impl that can Err; refactors of the packfile format are the realistic way this invariant breaks.

Related errors


AI-assisted analysis of atuinsh/atuin@15fe1318f1 (2026-08-19). Data as JSON: /api/errors/29ac0ba97573112b. Report an issue: GitHub.