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
- Treat a crash here as a code regression, not an environment problem: inspect recent changes to PackIA's fields
- When adding fields to PackIA, keep them to strings/UUID newtypes/numbers, or switch this call to real error handling with map_err
- 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
- Keep PackIA fields limited to infallible serde types (strings, UUID newtypes, numbers)
- Revisit this expect whenever the packfile manifest layout is extended
- Add a serde round-trip test for PackIA in the packfile test suite
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
- issue in stats average query
- issue in stats exits query
- issue in stats day of week query
- issue in stats duration over time query
- the vector is not empty
AI-assisted analysis of atuinsh/atuin@15fe1318f1 (2026-08-19).
Data as JSON: /api/errors/29ac0ba97573112b.
Report an issue: GitHub.