Skip to content

refactor: commonize MessagePack (rmp) helpers - #3665

Open
markovejnovic wants to merge 27 commits into
mainfrom
ai-wip/commonize-rmp
Open

refactor: commonize MessagePack (rmp) helpers#3665
markovejnovic wants to merge 27 commits into
mainfrom
ai-wip/commonize-rmp

Conversation

@markovejnovic

@markovejnovic markovejnovic commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

This PR commonizes some code regarding rmp ^(de)?serializers$.

  • atuin-scripts no longer panics on deserialize failures.
  • there were 6 error_report copy-pastas, now there's one DecodeError.
  • there were 7 checks for trailing bytes in the serialized rmp bytes. now there are some nice utilities to help against evil code
  • there are a lot more tests now

Downside is:

  • the caller needs to juggle rmp:: and atuin_common::rmp::

@markovejnovic markovejnovic changed the title AI WIP | Commonize MessagePack (rmp) helpers into atuin-common as extension traits AI WIP | Commonize MessagePack (rmp) helpers into atuin-common Jul 19, 2026
@markovejnovic markovejnovic changed the title AI WIP | Commonize MessagePack (rmp) helpers into atuin-common chore: commonize MessagePack (rmp) helpers Jul 19, 2026
@markovejnovic markovejnovic changed the title chore: commonize MessagePack (rmp) helpers refactor: commonize MessagePack (rmp) helpers Jul 19, 2026
@markovejnovic
markovejnovic marked this pull request as ready for review July 19, 2026 11:05
@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Centralizes MessagePack encode/decode helpers from six scattered copy-pastas into atuin-common::rmp, with a DecodeError type, DecodeExt trait, and helpers like read_total_array / expect_eof. The critical improvement is replacing all .unwrap() calls in atuin-scripts deserialization with proper ?-based error propagation.

  • atuin-common::rmp is introduced as an optional feature (rmp, eyre); consumers opt in via Cargo.toml features.
  • read_total_array / read_array_of / expect_eof / expect_array_len abstract the previously copy-pasted trailing-byte checks; proptest round-trip tests cover the new helpers.
  • The PR acknowledges callers must juggle both rmp:: and atuin_common::rmp:: namespaces; this is a side effect of not re-exporting the raw rmp primitives.

Confidence Score: 4/5

Safe to merge — serialization format is unchanged and all callers produce and consume identical bytes.

The change touches serialization/deserialization paths across eight crates. No format changes, logic is preserved, and new proptest round-trips provide good coverage. The sole note is an omitted expect_eof in ScriptRecord::Delete that pre-dates this PR and is not a regression.

crates/atuin-scripts/src/store/record.rs — the Delete arm does not call expect_eof, unlike the parallel HistoryRecord::Delete path updated in this PR.

Important Files Changed

Filename Overview
crates/atuin-common/src/rmp/decode.rs New decode helper module: DecodeError, DecodeExt trait, read_string, read_optional, read_total_array, read_array_of, expect_array_len, expect_eof — well-structured and correct
crates/atuin-common/src/rmp/encode.rs New encode wrapper module with EncodeError; thin wrappers over rmp::encode with consistent error type
crates/atuin-common/src/rmp.rs Module root wiring decode/encode/eyre submodules; proptest + rstest round-trip tests look correct
crates/atuin-scripts/src/store/record.rs Old unwrap-heavy deserialize replaced with ?-based error propagation; Delete arm does not call expect_eof (pre-existing omission, not regressed here)
crates/atuin-scripts/src/store/script.rs All unwrap() calls replaced with proper error propagation via read_total_array + read_array_of; panic-on-decode eliminated
crates/atuin-client/src/history.rs Migrated to atu_rmp helpers; logic and field order preserved; read_int replaces typed read functions consistently
crates/atuin-client/src/encryption.rs Key encode/decode migrated to atu_rmp; Marker matching preserved; expect_array_len used for Array16 path
crates/atuin-kv/src/store/record.rs v0 and v1 deserialization rewritten with read_total_array; has_value conditional read preserved correctly
crates/atuin-dotfiles/src/store.rs AliasRecord serialize/deserialize migrated cleanly with read_total_array for create/delete arms
crates/atuin-dotfiles/src/store/var.rs VarRecord migrated; new encode_decode_delete test added
crates/atuin-client/src/utils/rmp.rs Deleted — functionality moved to atuin-common::rmp

Reviews (1): Last reviewed commit: "more cleanup" | Re-trigger Greptile

Comment on lines 66 to 69
1 => {
let bytes = bytes.remaining_slice();
let (id, _) = decode::read_str_from_slice(bytes).map_err(error_report)?;
Ok(ScriptRecord::Delete(Uuid::parse_str(id)?))
let id = atu_rmp::decode::read_string(&mut bytes).decode()?;
Ok(ScriptRecord::Delete(Uuid::parse_str(&id)?))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing EOF check on Delete arm. The HistoryRecord::Delete path in history/store.rs was updated in this PR to call expect_eof, but the equivalent ScriptRecord::Delete arm here was not. A truncated or padded payload would silently succeed as long as the UUID is valid. The old code also dropped trailing bytes with let (id, _) = ..., so this isn't a regression — but it's inconsistent with the treatment of the other Delete arms in this PR.

@taylordotfish taylordotfish left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why a bunch of comments were changed, imo most are worse now

also prefer type parameters and where clauses to impl Trait, not enough to block a PR but it's weird that they were changed

also not sure why a bunch of the functions take Bytes instead of a generic RmpRead or RmpWrite, I think they were better before

diff is also larger than necessary, imo this PR changes a bunch of things that didn't need changing

Comment on lines +52 to +62
pub fn write_optional<W: RmpWrite, T>(
out: &mut W,
value: Option<T>,
write: impl FnOnce(&mut W, T) -> Result<(), EncodeError<W::Error>>,
) -> Result<(), EncodeError<W::Error>> {
match value {
Some(v) => write(out, v),
None => rmp::encode::write_nil(out)
.map_err(|e| EncodeError::from(ValueWriteError::InvalidMarkerWrite(e))),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you keep this function the way it was before?

Comment on lines +7 to +51
pub fn write_u8<W: RmpWrite>(out: &mut W, val: u8) -> Result<(), EncodeError<W::Error>> {
rmp::encode::write_u8(out, val)?;
Ok(())
}

pub fn write_u16<W: RmpWrite>(out: &mut W, val: u16) -> Result<(), EncodeError<W::Error>> {
rmp::encode::write_u16(out, val)?;
Ok(())
}

pub fn write_u64<W: RmpWrite>(out: &mut W, val: u64) -> Result<(), EncodeError<W::Error>> {
rmp::encode::write_u64(out, val)?;
Ok(())
}

pub fn write_uint<W: RmpWrite>(out: &mut W, val: u64) -> Result<(), EncodeError<W::Error>> {
rmp::encode::write_uint(out, val)?;
Ok(())
}

pub fn write_sint<W: RmpWrite>(out: &mut W, val: i64) -> Result<(), EncodeError<W::Error>> {
rmp::encode::write_sint(out, val)?;
Ok(())
}

pub fn write_bool<W: RmpWrite>(out: &mut W, val: bool) -> Result<(), EncodeError<W::Error>> {
rmp::encode::write_bool(out, val)
.map_err(|e| EncodeError::from(ValueWriteError::InvalidMarkerWrite(e)))
}

pub fn write_str<W: RmpWrite>(out: &mut W, data: &str) -> Result<(), EncodeError<W::Error>> {
rmp::encode::write_str(out, data)?;
Ok(())
}

pub fn write_bin<W: RmpWrite>(out: &mut W, data: &[u8]) -> Result<(), EncodeError<W::Error>> {
rmp::encode::write_bin(out, data)?;
Ok(())
}

pub fn write_array_len<W: RmpWrite>(out: &mut W, len: u32) -> Result<(), EncodeError<W::Error>> {
rmp::encode::write_array_len(out, len)?;
Ok(())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we generate these with a macro instead?

Comment on lines +74 to +115
/// Read an owned [`String`].
pub fn read_string<'a>(
bytes: &mut Bytes<'a>,
) -> Result<String, DecodeStringError<'a, BytesReadError>> {
let slice = bytes.remaining_slice();
let (string, rest) = match mp::read_str_from_slice(slice) {
Ok(pair) => pair,
Err(e) => {
if let DecodeStringError::TypeMismatch(_) = e {
// rmp consumes the marker byte on a type mismatch; match that so
// `read_optional` can detect nil.
bytes
.read_u8()
.expect("TypeMismatch implies the stream contains a marker byte");
}
return Err(e);
}
};
*bytes = Bytes::new(rest);
Ok(string.into())
}

/// Read a value that may be nil, returning [`None`] for nil.
pub fn read_optional<'a, T, E>(
bytes: &mut Bytes<'a>,
read: impl FnOnce(&mut Bytes<'a>) -> Result<T, E>,
) -> Result<Option<T>, DecodeError<'a>>
where
E: Into<DecodeError<'a>>,
{
match read(bytes) {
Ok(v) => Ok(Some(v)),
Err(e) => {
let e = e.into();
if let Some(Marker::Null) = e.type_mismatch() {
Ok(None)
} else {
Err(e)
}
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we keep these functions the way they were before?

Comment on lines +220 to +232
atu_rmp::encode::write_u16(&mut output, Version::LATEST.as_int())?;
atu_rmp::encode::write_array_len(&mut output, Version::LATEST.min_fields())?;

atu_rmp::encode::write_str(&mut output, &self.id.0)?;
atu_rmp::encode::write_u64(&mut output, self.timestamp.unix_timestamp_nanos() as u64)?;
atu_rmp::encode::write_sint(&mut output, self.duration)?;
atu_rmp::encode::write_sint(&mut output, self.exit)?;
atu_rmp::encode::write_str(&mut output, &self.command)?;
atu_rmp::encode::write_str(&mut output, &self.cwd)?;
atu_rmp::encode::write_str(&mut output, &self.session)?;
atu_rmp::encode::write_str(&mut output, &self.hostname)?;

atu_rmp::encode::write_optional(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we import encode and decode like before to minimize the diff?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants