From 35a01898e6fbe598b57d4beaf452b58a676062c9 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Tue, 10 Feb 2026 09:11:19 +0000 Subject: [PATCH] core: guard on expected multibase When decoding an input string for a `RepoId`, the base was never checked. This change ensures that the base is checked and will error otherwise. To allow for future base codes, the change introduces a sanctioned set of base codes, which only contains `Base58Btc` for now. Tests are added to ensure that parsing is correct and includes a valid multibase code 'Z', which is not the expected 'z'. --- crates/radicle-core/src/repo.rs | 90 ++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/crates/radicle-core/src/repo.rs b/crates/radicle-core/src/repo.rs index 347ba18d..d567b65c 100644 --- a/crates/radicle-core/src/repo.rs +++ b/crates/radicle-core/src/repo.rs @@ -1,3 +1,4 @@ +use alloc::fmt; use alloc::string::String; use alloc::string::ToString as _; use alloc::vec::Vec; @@ -8,12 +9,34 @@ use thiserror::Error; /// Radicle identifier prefix. pub const RAD_PREFIX: &str = "rad:"; +#[non_exhaustive] #[derive(Error, Debug)] pub enum IdError { #[error(transparent)] Multibase(#[from] multibase::Error), #[error("invalid length: expected {expected} bytes, got {actual} bytes")] Length { expected: usize, actual: usize }, + #[error(fmt = fmt_mismatched_base_encoding)] + MismatchedBaseEncoding { + input: String, + expected: Vec, + found: multibase::Base, + }, +} + +fn fmt_mismatched_base_encoding( + input: &String, + expected: &[multibase::Base], + found: &multibase::Base, + formatter: &mut fmt::Formatter, +) -> fmt::Result { + write!( + formatter, + "invalid multibase encoding '{}' for '{}', expected one of {:?}", + found.code(), + input, + expected.iter().map(|base| base.code()).collect::>() + ) } /// A repository identifier. @@ -43,6 +66,8 @@ impl core::fmt::Debug for RepoId { } impl RepoId { + const ALLOWED_BASES: [multibase::Base; 1] = [multibase::Base::Base58Btc]; + /// Format the identifier as a human-readable URN. /// /// Eg. `rad:z3XncAdkZjeK9mQS5Sdc4qhw98BUX`. @@ -73,7 +98,8 @@ impl RepoId { pub fn from_canonical(input: &str) -> Result { const EXPECTED_LEN: usize = 20; - let (_, bytes) = multibase::decode(input)?; + let (base, bytes) = multibase::decode(input)?; + Self::guard_base_encoding(input, base)?; let bytes: [u8; EXPECTED_LEN] = bytes.try_into().map_err(|bytes: Vec| IdError::Length { expected: EXPECTED_LEN, @@ -81,6 +107,18 @@ impl RepoId { })?; Ok(Self(Oid::from_sha1(bytes))) } + + fn guard_base_encoding(input: &str, base: multibase::Base) -> Result<(), IdError> { + if !Self::ALLOWED_BASES.contains(&base) { + Err(IdError::MismatchedBaseEncoding { + input: input.to_string(), + expected: Self::ALLOWED_BASES.to_vec(), + found: base, + }) + } else { + Ok(()) + } + } } impl core::str::FromStr for RepoId { @@ -295,4 +333,54 @@ mod test { prop_roundtrip_parse(rid) } } + + #[test] + fn invalid() { + assert!("".parse::().is_err()); + assert!("not-a-valid-rid".parse::().is_err()); + assert!("xyz:z3gqcJUoA1n9HaHKufZs5FCSGazv5" + .parse::() + .is_err()); + assert!("RAD:z3gqcJUoA1n9HaHKufZs5FCSGazv5" + .parse::() + .is_err()); + assert!("rad:".parse::().is_err()); + assert!("rad:z3gqcJUoA1n9HaHKufZs5FCSG0zv5" + .parse::() + .is_err()); + assert!("rad:z3gqcJUoA1n9HaHKufZs5FCSGOzv5" + .parse::() + .is_err()); + assert!("rad:z3gqcJUoA1n9HaHKufZs5FCSGIzv5" + .parse::() + .is_err()); + assert!("rad:z3gqcJUoA1n9HaHKufZs5FCSGlzv5" + .parse::() + .is_err()); + assert!("rad:z3gqcJUoA1n9HaHKufZs5FCSGázv5" + .parse::() + .is_err()); + assert!("rad:z3gqcJUoA1n9HaHKufZs5FCSG@zv5" + .parse::() + .is_err()); + assert!("rad:Z3gqcJUoA1n9HaHKufZs5FCSGazv5" + .parse::() + .is_err()); + assert!("rad:z3gqcJUoA1n9HaHKuf".parse::().is_err()); + assert!("rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5abcdef" + .parse::() + .is_err()); + assert!("rad: z3gqcJUoA1n9HaHKufZs5FCSGazv5" + .parse::() + .is_err()); + } + + #[test] + fn valid() { + assert!("rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" + .parse::() + .is_ok()); + assert!("z3gqcJUoA1n9HaHKufZs5FCSGazv5".parse::().is_ok()); + assert!("z3XncAdkZjeK9mQS5Sdc4qhw98BUX".parse::().is_ok()); + } }