diff --git a/crates/atuin-client/record-migrations/20230531212437_create-records.sql b/crates/atuin-client/record-migrations/20230531212437_create-records.sql deleted file mode 100644 index 4f4b304a..00000000 --- a/crates/atuin-client/record-migrations/20230531212437_create-records.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Add migration script here -create table if not exists records ( - id text primary key, - parent text unique, -- null if this is the first one - host text not null, - - timestamp integer not null, - tag text not null, - version text not null, - data blob not null, - cek blob not null -); - -create index host_idx on records (host); -create index tag_idx on records (tag); -create index host_tag_idx on records (host, tag); diff --git a/crates/atuin-client/record-migrations/20231127090831_create-store.sql b/crates/atuin-client/record-migrations/20231127090831_create-store.sql deleted file mode 100644 index 53d78860..00000000 --- a/crates/atuin-client/record-migrations/20231127090831_create-store.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Add migration script here -create table if not exists store ( - id text primary key, -- globally unique ID - - idx integer, -- incrementing integer ID unique per (host, tag) - host text not null, -- references the host row - tag text not null, - - timestamp integer not null, - version text not null, - data blob not null, - cek blob not null -); - -create unique index record_uniq ON store(host, tag, idx); diff --git a/crates/atuin-client/src/history.rs b/crates/atuin-client/src/history.rs index e1304821..0c0a5782 100644 --- a/crates/atuin-client/src/history.rs +++ b/crates/atuin-client/src/history.rs @@ -1,16 +1,10 @@ -use rmp::decode::{self, Bytes}; -use rmp::encode; use std::env; -use atuin_common::record::DecryptedData; use atuin_common::utils::{normalize_optional_string, uuid_v7}; -use eyre::{Result, bail}; - use crate::secrets::SECRET_PATTERNS_RE; use crate::settings::Settings; use crate::utils::get_host_user; -use crate::utils::rmp::{DecodeError, EncodeError, read_optional, read_string, write_optional}; use time::OffsetDateTime; pub(crate) mod builder; @@ -203,116 +197,6 @@ impl History { } } - /// Serializes a history entry in the V2 format. - /// - /// Differences from V1: - /// - /// * `intent` is always written; if `None`, nil is written to the output. - /// * Added new field `shell`. - /// - /// V2 is designed to allow new fields to be added without incrementing the version. V1 cannot - /// accommodate this because its deserialization routine errors if more than 11 fields are - /// provided. - pub fn serialize(&self) -> Result { - let mut output = vec![]; - - // write the version - encode::write_u16(&mut output, Version::LATEST.as_int())?; - encode::write_array_len(&mut output, Version::LATEST.min_fields())?; - - encode::write_str(&mut output, &self.id.0)?; - encode::write_u64(&mut output, self.timestamp.unix_timestamp_nanos() as u64)?; - encode::write_sint(&mut output, self.duration)?; - encode::write_sint(&mut output, self.exit)?; - encode::write_str(&mut output, &self.command)?; - encode::write_str(&mut output, &self.cwd)?; - encode::write_str(&mut output, &self.session)?; - encode::write_str(&mut output, &self.hostname)?; - - write_optional( - &mut output, - self.deleted_at.map(|d| d.unix_timestamp_nanos() as u64), - encode::write_u64, - )?; - encode::write_str(&mut output, self.author.as_str())?; - write_optional(&mut output, self.intent.as_deref(), encode::write_str)?; - write_optional(&mut output, self.shell.as_deref(), encode::write_str)?; - Ok(DecryptedData(output)) - } - - pub fn deserialize(bytes: &[u8], version: &str) -> Result { - let Some(version) = Version::from_name(version) else { - bail!("unknown version {version:?}"); - }; - - let mut bytes = Bytes::new(bytes); - - let real_version = decode::read_u16(&mut bytes).map_err(DecodeError::from)?; - if real_version != version.as_int() { - bail!("expected to decode {version} record, found v{real_version}"); - } - - let nfields = decode::read_array_len(&mut bytes).map_err(DecodeError::from)?; - let min_fields = version.min_fields(); - if nfields < min_fields || version.max_fields().is_some_and(|max| nfields > max) { - bail!("unexpected number of fields ({nfields}) for history version {version}"); - } - - let id = read_string(&mut bytes)?; - let timestamp = decode::read_u64(&mut bytes).map_err(DecodeError::from)?; - let duration = decode::read_int(&mut bytes).map_err(DecodeError::from)?; - let exit = decode::read_int(&mut bytes).map_err(DecodeError::from)?; - - let command = read_string(&mut bytes)?; - let cwd = read_string(&mut bytes)?; - let session = read_string(&mut bytes)?; - let hostname = read_string(&mut bytes)?; - let deleted_at = read_optional(&mut bytes, decode::read_u64)?; - - let author = if version >= Version::One { - read_optional(&mut bytes, read_string)? - } else { - None - }; - - let intent = if match version { - Version::Zero => false, - Version::One => nfields > min_fields, - Version::Two => true, - } { - read_optional(&mut bytes, read_string)? - } else { - None - }; - - let shell = if version >= Version::Two { - read_optional(&mut bytes, read_string)? - } else { - None - }; - - if version < Version::Two && !bytes.remaining_slice().is_empty() { - bail!("trailing bytes in encoded history. malformed") - } - - Ok(History { - id: id.into(), - timestamp: OffsetDateTime::from_unix_timestamp_nanos(i128::from(timestamp))?, - duration, - exit, - command, - cwd, - session, - author: author.unwrap_or_else(|| Self::author_from_hostname(&hostname)), - hostname, - intent, - deleted_at: deleted_at - .map(|t| OffsetDateTime::from_unix_timestamp_nanos(i128::from(t))) - .transpose()?, - shell, - }) - } - /// Builder for a history entry that is imported from shell history. /// /// The only two required fields are `timestamp` and `command`. @@ -477,10 +361,9 @@ impl History { #[cfg(test)] mod tests { use regex::RegexSet; - use time::macros::datetime; use crate::{ - history::{AUTHOR_FILTER_ALL_AGENT, AUTHOR_FILTER_ALL_USER, Version}, + history::{AUTHOR_FILTER_ALL_AGENT, AUTHOR_FILTER_ALL_USER}, settings::Settings, }; @@ -574,159 +457,4 @@ mod tests { assert!(stripe_key.should_save(&settings)); } - - #[test] - fn test_serialize_deserialize() { - let history = History { - id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), - timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), - duration: 49206000, - exit: 0, - command: "git status".to_owned(), - cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), - session: "b97d9a306f274473a203d2eba41f9457".to_owned(), - hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), - author: "conrad.ludgate".to_owned(), - intent: None, - deleted_at: None, - shell: None, - }; - - let serialized = history.serialize().expect("failed to serialize history"); - assert_eq!( - &serialized.0[0..3], - [205, 0, 2], - "should encode as history v2" - ); - - let deserialized = History::deserialize(&serialized.0, Version::LATEST.name()) - .expect("failed to deserialize history"); - assert_eq!(history, deserialized); - } - - #[test] - fn test_serialize_deserialize_deleted() { - let history = History { - id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), - timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), - duration: 49206000, - exit: 0, - command: "git status".to_owned(), - cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), - session: "b97d9a306f274473a203d2eba41f9457".to_owned(), - hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), - author: "conrad.ludgate".to_owned(), - intent: None, - deleted_at: Some(datetime!(2023-11-19 20:18 +00:00)), - shell: Some("bash".into()), - }; - - let serialized = history.serialize().expect("failed to serialize history"); - - let deserialized = History::deserialize(&serialized.0, Version::LATEST.name()) - .expect("failed to deserialize history"); - - assert_eq!(history, deserialized); - } - - #[test] - fn test_serialize_deserialize_with_author_and_intent() { - let history = History { - id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), - timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), - duration: 49206000, - exit: 0, - command: "git status".to_owned(), - cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), - session: "b97d9a306f274473a203d2eba41f9457".to_owned(), - hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), - author: "claude".to_owned(), - intent: Some("check repository status".to_owned()), - deleted_at: None, - shell: Some("fish".into()), - }; - - let serialized = history.serialize().expect("failed to serialize history"); - let deserialized = History::deserialize(&serialized.0, Version::LATEST.name()) - .expect("failed to deserialize history"); - - assert_eq!(history, deserialized); - } - - #[test] - fn test_serialize_deserialize_version() { - let bytes_v0 = [ - 205, 0, 0, 153, 217, 32, 54, 54, 100, 49, 54, 99, 98, 101, 101, 55, 99, 100, 52, 55, - 53, 51, 56, 101, 53, 99, 53, 98, 56, 98, 52, 52, 101, 57, 48, 48, 54, 101, 207, 23, 99, - 98, 117, 24, 210, 246, 128, 206, 2, 238, 210, 240, 0, 170, 103, 105, 116, 32, 115, 116, - 97, 116, 117, 115, 217, 42, 47, 85, 115, 101, 114, 115, 47, 99, 111, 110, 114, 97, 100, - 46, 108, 117, 100, 103, 97, 116, 101, 47, 68, 111, 99, 117, 109, 101, 110, 116, 115, - 47, 99, 111, 100, 101, 47, 97, 116, 117, 105, 110, 217, 32, 98, 57, 55, 100, 57, 97, - 51, 48, 54, 102, 50, 55, 52, 52, 55, 51, 97, 50, 48, 51, 100, 50, 101, 98, 97, 52, 49, - 102, 57, 52, 53, 55, 187, 102, 118, 102, 103, 57, 51, 54, 99, 48, 107, 112, 102, 58, - 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, 116, 101, 192, - ]; - - let bytes_v1 = [ - 205, 0, 1, 155, 217, 32, 54, 54, 100, 49, 54, 99, 98, 101, 101, 55, 99, 100, 52, 55, - 53, 51, 56, 101, 53, 99, 53, 98, 56, 98, 52, 52, 101, 57, 48, 48, 54, 101, 207, 23, 99, - 98, 117, 24, 210, 246, 128, 206, 2, 238, 210, 240, 0, 170, 103, 105, 116, 32, 115, 116, - 97, 116, 117, 115, 217, 42, 47, 85, 115, 101, 114, 115, 47, 99, 111, 110, 114, 97, 100, - 46, 108, 117, 100, 103, 97, 116, 101, 47, 68, 111, 99, 117, 109, 101, 110, 116, 115, - 47, 99, 111, 100, 101, 47, 97, 116, 117, 105, 110, 217, 32, 98, 57, 55, 100, 57, 97, - 51, 48, 54, 102, 50, 55, 52, 52, 55, 51, 97, 50, 48, 51, 100, 50, 101, 98, 97, 52, 49, - 102, 57, 52, 53, 55, 187, 102, 118, 102, 103, 57, 51, 54, 99, 48, 107, 112, 102, 58, - 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, 116, 101, 207, 24, 194, 83, - 235, 108, 206, 10, 0, 174, 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, 116, - 101, 173, 115, 97, 109, 112, 108, 101, 32, 105, 110, 116, 101, 110, 116, - ]; - - let expected_v2 = History { - id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), - timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), - duration: 49206000, - exit: 0, - command: "git status".to_owned(), - cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), - session: "b97d9a306f274473a203d2eba41f9457".to_owned(), - hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), - author: "conrad.ludgate".to_owned(), - intent: Some("sample intent".to_owned()), - deleted_at: Some(time::OffsetDateTime::from_unix_timestamp(1784080673).unwrap()), - shell: Some("zsh".into()), - }; - let bytes_v2 = expected_v2 - .serialize() - .expect("failed to serialize history"); - - let mut expected_v1 = expected_v2.clone(); - expected_v1.shell = None; - - let mut expected_v0 = expected_v1.clone(); - expected_v0.intent = None; - expected_v0.deleted_at = None; - - let cases = [ - (bytes_v0.as_slice(), expected_v0), - (&bytes_v1, expected_v1), - (&bytes_v2, expected_v2), - ]; - - for (i, (bytes, expected)) in cases.into_iter().enumerate() { - for version in Version::VARIANTS { - let deserialized = History::deserialize(bytes, version.name()); - if usize::from(version.as_int()) == i { - let Ok(deserialized) = deserialized else { - panic!("failed to deserialize {version}"); - }; - assert_eq!(deserialized, expected, "{version}"); - } else { - assert!( - deserialized.is_err(), - "unexpected success deserializing as {version}" - ); - } - } - } - } } diff --git a/crates/atuin-client/src/utils.rs b/crates/atuin-client/src/utils.rs index 236abc7e..35d7db26 100644 --- a/crates/atuin-client/src/utils.rs +++ b/crates/atuin-client/src/utils.rs @@ -1,5 +1,3 @@ -pub(crate) mod rmp; - pub(crate) fn get_hostname() -> String { std::env::var("ATUIN_HOST_NAME") .unwrap_or_else(|_| whoami::hostname().unwrap_or_else(|_| "unknown-host".to_string())) diff --git a/crates/atuin-client/src/utils/rmp.rs b/crates/atuin-client/src/utils/rmp.rs deleted file mode 100644 index 43b93d92..00000000 --- a/crates/atuin-client/src/utils/rmp.rs +++ /dev/null @@ -1,123 +0,0 @@ -use rmp::Marker; -use rmp::decode::bytes::{Bytes, BytesReadError}; -use rmp::decode::{ - self, DecodeStringError, NumValueReadError, RmpRead, RmpReadErr, ValueReadError, -}; -use rmp::encode::{self, RmpWrite, RmpWriteErr, ValueWriteError}; - -/// An error encountered while trying to encode a message with [`rmp`]. -/// -/// This is currently just a wrapper around [`ValueWriteError`] with a better error message. -/// [`rmp`]'s error message does not indicate which variant the error is (`InvalidMarkerWrite` or -/// `InvalidDataWrite`) and does not print anything about the inner I/O error of type `E`. -#[derive(Debug, derive_more::Display, derive_more::From, thiserror::Error)] -#[display("could not write MessagePack value: {_0:?}")] -pub struct EncodeError(ValueWriteError); - -/// An error encountered while trying to decode a message with [`rmp`]. -/// -/// This is a wrapper the various types of errors that can be returned by [`rmp`]'s decoding -/// functions. Unlike those types, this type implements [`Display`] with an error message that -/// indicates which variant the error is ([`rmp`]'s error types are enums; some unconditionally -/// print a static string and others don't even implement [`Display`] for all `E`). -/// -/// Conversion to [`eyre::Report`] is supported. This cannot be done by implementing -/// [`std::error::Error`] because this type is not, in general, `'static`, so a manual -/// implementation is provided. -#[derive(Debug, derive_more::Display, derive_more::From)] -#[display("could not decode MessagePack value: {_0:?}")] -pub enum DecodeError<'a, E: RmpReadErr = BytesReadError> { - DecodeString(DecodeStringError<'a, E>), - NumValueRead(NumValueReadError), - ValueRead(ValueReadError), -} - -impl DecodeError<'_, E> { - pub fn type_mismatch(&self) -> Option { - match self { - Self::DecodeString(DecodeStringError::TypeMismatch(m)) => Some(*m), - Self::NumValueRead(NumValueReadError::TypeMismatch(m)) => Some(*m), - Self::ValueRead(ValueReadError::TypeMismatch(m)) => Some(*m), - _ => None, - } - } -} - -impl From> for eyre::Report { - fn from(e: DecodeError<'_, E>) -> Self { - eyre::eyre!("{e}") - } -} - -/// Read an owned string from a [`Bytes`] object. -/// -/// If you need an owned [`String`], this function is more convenient than using -/// [`read_str_from_slice`] and converting the resulting [`str`], as you don't need to -/// keep unwrapping and re-creating the [`Bytes`] object. -/// -/// [`read_str_from_slice`]: decode::read_str_from_slice -pub fn read_string<'a>(bytes: &mut Bytes<'a>) -> Result> { - let slice = bytes.remaining_slice(); - let (string, rest) = match decode::read_str_from_slice(slice) { - Ok(pair) => pair, - Err(e) => { - if let DecodeStringError::TypeMismatch(_) = e { - // The decode functions in `rmp::decode` consume the marker byte when there's a - // type mismatch; make sure we do that too, as `read_optional` depends on it. - bytes - .read_u8() - .expect("TypeMismatch implies stream contains a marker byte"); - } - return Err(e.into()); - } - }; - *bytes = Bytes::new(rest); - Ok(string.into()) -} - -/// Read an optional value from the stream. -/// -/// This function calls `read`, which should try to decode a value of type `T` from the stream. If -/// that function returns an error indicating [`Marker::Null`] was encountered instead, this -/// function returns [`None`]. All other errors are forwarded as-is. -pub fn read_optional<'a, R, F, T, E>( - input: &mut R, - read: F, -) -> Result, DecodeError<'a, R::Error>> -where - R: RmpRead, - R::Error: Send + Sync, - F: FnOnce(&mut R) -> Result, - E: Into>, -{ - let err = match read(input) { - Ok(v) => return Ok(Some(v)), - Err(e) => e.into(), - }; - - if let Some(Marker::Null) = err.type_mismatch() { - Ok(None) - } else { - Err(err) - } -} - -/// Write an optional value to the stream. -/// -/// If `value` is [`Some`], this function calls `write` with the value, which should encode a value -/// of type `T` to the stream. Otherwise, this function writes [`Marker::Null`]. -pub fn write_optional( - output: &mut W, - value: Option, - write: F, -) -> Result<(), ValueWriteError> -where - W: RmpWrite, - F: FnOnce(&mut W, T) -> Result<(), ValueWriteError>, - W::Error: Send + Sync, -{ - match value { - Some(v) => write(output, v), - None => encode::write_nil(output).map_err(ValueWriteError::InvalidMarkerWrite), - } -} diff --git a/crates/atuin-common/src/lib.rs b/crates/atuin-common/src/lib.rs index 016d4d3a..5b05901b 100644 --- a/crates/atuin-common/src/lib.rs +++ b/crates/atuin-common/src/lib.rs @@ -1,69 +1,9 @@ #![deny(unsafe_code)] -/// Defines a new UUID type wrapper -macro_rules! new_uuid { - ($name:ident) => { - #[derive( - Debug, - Copy, - Clone, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - serde::Serialize, - serde::Deserialize, - derive_more::Display, - derive_more::From, - derive_more::Deref, - )] - #[serde(transparent)] - #[display("{_0}")] - pub struct $name(pub Uuid); - - impl sqlx::Type for $name - where - Uuid: sqlx::Type, - { - fn type_info() -> ::TypeInfo { - Uuid::type_info() - } - } - - impl<'r, DB: sqlx::Database> sqlx::Decode<'r, DB> for $name - where - Uuid: sqlx::Decode<'r, DB>, - { - fn decode( - value: DB::ValueRef<'r>, - ) -> std::result::Result { - Uuid::decode(value).map(Self) - } - } - - impl<'q, DB: sqlx::Database> sqlx::Encode<'q, DB> for $name - where - Uuid: sqlx::Encode<'q, DB>, - { - fn encode_by_ref( - &self, - buf: &mut DB::ArgumentBuffer<'q>, - ) -> Result> - { - self.0.encode_by_ref(buf) - } - } - }; -} - pub mod api; pub mod logs; -pub mod record; pub mod shell; pub mod string; #[cfg(feature = "test-utils")] pub mod test_utils; -pub mod tls; -pub mod url; pub mod utils; diff --git a/crates/atuin-common/src/tls.rs b/crates/atuin-common/src/tls.rs deleted file mode 100644 index e8c840e0..00000000 --- a/crates/atuin-common/src/tls.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::sync::Once; - -static INIT: Once = Once::new(); - -/// Ensure the rustls crypto provider (ring) is installed. -/// -/// Must be called before creating any reqwest clients. Safe to call -/// multiple times — only the first call installs the provider. -pub fn ensure_crypto_provider() { - INIT.call_once(|| { - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install rustls crypto provider"); - }); -}