use crate::{Error, Result};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GroupId([u8; Self::LEN]);
impl GroupId {
pub const LEN: usize = 16;
pub const TOP: Self = Self([0; Self::LEN]);
pub fn generate() -> Result<Self> {
let mut bytes = [0_u8; Self::LEN];
crate::getrandom::fill(&mut bytes).map_err(|_| Error::RandomnessUnavailable)?;
Ok(Self(bytes))
}
#[must_use]
pub const fn from_bytes(bytes: [u8; Self::LEN]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; Self::LEN] {
&self.0
}
#[must_use]
pub const fn is_top(&self) -> bool {
let mut index = 0;
while index < Self::LEN {
if self.0[index] != 0 {
return false;
}
index += 1;
}
true
}
}
impl core::fmt::Debug for GroupId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.is_top() {
return f.write_str("GroupId(TOP)");
}
write!(f, "GroupId(")?;
for byte in &self.0 {
write!(f, "{byte:02x}")?;
}
f.write_str(")")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Group {
pub name: String,
pub group_order: u32,
pub entry_ids: Vec<crate::vault::EntryId>,
}
impl Group {
pub const MAX_ENTRIES: usize = crate::vault::Entry::MAX_FIELD_LEN / GroupId::LEN;
pub fn new(
name: String,
group_order: u32,
entry_ids: Vec<crate::vault::EntryId>,
) -> Result<Self> {
if name.len() > crate::vault::Entry::MAX_FIELD_LEN {
return Err(Error::FieldTooLong {
field: "group name",
limit: crate::vault::Entry::MAX_FIELD_LEN,
actual: name.len(),
});
}
if entry_ids.len() > Self::MAX_ENTRIES {
return Err(Error::VaultGroupTooManyEntries {
limit: Self::MAX_ENTRIES,
actual: entry_ids.len(),
});
}
if first_duplicate(&entry_ids).is_some() {
return Err(Error::VaultGroupDuplicateEntry);
}
Ok(Self {
name,
group_order,
entry_ids,
})
}
pub fn top(entry_ids: Vec<crate::vault::EntryId>) -> Result<Self> {
Self::new(String::new(), 0, entry_ids)
}
pub(crate) fn top_empty() -> Self {
Self {
name: String::new(),
group_order: 0,
entry_ids: Vec::new(),
}
}
#[must_use]
pub const fn is_top_name(&self) -> bool {
self.name.is_empty()
}
}
fn first_duplicate(ids: &[crate::vault::EntryId]) -> Option<crate::vault::EntryId> {
for (index, id) in ids.iter().enumerate() {
if ids[index + 1..].contains(id) {
return Some(*id);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::EntryId;
fn id(seed: u8) -> EntryId {
EntryId::from_bytes([seed; EntryId::LEN])
}
#[test]
fn the_top_identifier_is_reserved_and_recognized() {
assert!(GroupId::TOP.is_top());
assert_eq!(GroupId::TOP.as_bytes(), &[0_u8; GroupId::LEN]);
assert_eq!(format!("{:?}", GroupId::TOP), "GroupId(TOP)");
}
#[test]
fn a_generated_identifier_is_never_the_reserved_one() {
for _ in 0..64 {
let generated = GroupId::generate().expect("OS randomness available");
assert!(
!generated.is_top(),
"a generated identifier collided with the reserved value"
);
assert_eq!(
format!("{generated:?}").len(),
"GroupId()".len() + 2 * GroupId::LEN
);
}
}
#[test]
fn a_group_keeps_its_name_order_and_list() {
let group = Group::new("Work".to_owned(), 3, vec![id(1), id(2)]).expect("valid group");
assert_eq!(group.name, "Work");
assert_eq!(group.group_order, 3);
assert_eq!(group.entry_ids, vec![id(1), id(2)]);
assert!(!group.is_top_name());
assert!(Group::top(vec![]).expect("valid group").is_top_name());
}
#[test]
fn a_duplicate_identifier_in_one_list_is_rejected() {
let err = Group::new("Work".to_owned(), 0, vec![id(1), id(2), id(1)]);
assert!(
matches!(err, Err(Error::VaultGroupDuplicateEntry)),
"one entry cannot hold two positions in the same ordering"
);
assert!(
Group::new("Work".to_owned(), 0, vec![id(1), id(2), id(3)]).is_ok(),
"distinct identifiers are fine"
);
}
#[test]
fn an_over_long_name_or_list_is_rejected() {
let long = "n".repeat(crate::vault::Entry::MAX_FIELD_LEN + 1);
assert!(matches!(
Group::new(long, 0, vec![]),
Err(Error::FieldTooLong { .. })
));
let too_many: Vec<EntryId> = (0..=Group::MAX_ENTRIES)
.map(|index| {
let mut bytes = [0_u8; EntryId::LEN];
bytes[..8].copy_from_slice(
&u64::try_from(index)
.expect("the test range fits in u64")
.to_be_bytes(),
);
EntryId::from_bytes(bytes)
})
.collect();
assert!(
matches!(
Group::new(String::new(), 0, too_many),
Err(Error::VaultGroupTooManyEntries { .. })
),
"the list travels in one node, so it is bounded by MAX_FIELD_LEN"
);
}
}