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, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GroupChild {
Entry(crate::vault::EntryId),
Group(GroupId),
}
impl GroupChild {
#[must_use]
pub const fn id_bytes(&self) -> &[u8; GroupId::LEN] {
match self {
Self::Entry(id) => id.as_bytes(),
Self::Group(id) => id.as_bytes(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Group {
pub name: String,
pub children: Vec<GroupChild>,
}
impl Group {
pub const MAX_CHILDREN: usize = (crate::vault::Entry::MAX_FIELD_LEN - 5) / (1 + GroupId::LEN);
pub fn new(name: String, children: Vec<GroupChild>) -> 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 children.len() > Self::MAX_CHILDREN {
return Err(Error::VaultGroupTooManyChildren {
limit: Self::MAX_CHILDREN,
actual: children.len(),
});
}
if first_duplicate(&children).is_some() {
return Err(Error::VaultGroupDuplicateChild);
}
Ok(Self { name, children })
}
pub fn top(children: Vec<GroupChild>) -> Result<Self> {
Self::new(String::new(), children)
}
pub(crate) fn top_empty() -> Self {
Self {
name: String::new(),
children: Vec::new(),
}
}
#[must_use]
pub fn has_no_name(&self) -> bool {
self.name.is_empty()
}
#[must_use]
pub fn entry_ids(&self) -> Vec<crate::vault::EntryId> {
self.children
.iter()
.filter_map(|child| match child {
GroupChild::Entry(id) => Some(*id),
GroupChild::Group(_) => None,
})
.collect()
}
}
fn first_duplicate(children: &[GroupChild]) -> Option<GroupChild> {
for (index, child) in children.iter().enumerate() {
if children[index + 1..].contains(child) {
return Some(*child);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::EntryId;
fn id(seed: u8) -> EntryId {
EntryId::from_bytes([seed; EntryId::LEN])
}
fn gid(seed: u8) -> GroupId {
GroupId::from_bytes([seed; GroupId::LEN])
}
fn child(seed: u8) -> GroupChild {
GroupChild::Entry(id(seed))
}
#[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_and_children() {
let group = Group::new("Work".to_owned(), vec![child(1), GroupChild::Group(gid(2))])
.expect("valid group");
assert_eq!(group.name, "Work");
assert_eq!(group.children, vec![child(1), GroupChild::Group(gid(2))]);
assert_eq!(group.entry_ids(), vec![id(1)]);
assert!(!group.has_no_name());
assert!(Group::top(vec![]).expect("valid group").has_no_name());
}
#[test]
fn a_duplicate_child_in_one_list_is_rejected() {
let err = Group::new("Work".to_owned(), vec![child(1), child(2), child(1)]);
assert!(
matches!(err, Err(Error::VaultGroupDuplicateChild)),
"one child cannot hold two positions in the same ordering"
);
assert!(
Group::new("Work".to_owned(), vec![child(1), child(2), child(3)]).is_ok(),
"distinct references 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, vec![]),
Err(Error::FieldTooLong { .. })
));
let too_many: Vec<GroupChild> = (0..=Group::MAX_CHILDREN)
.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(),
);
GroupChild::Entry(EntryId::from_bytes(bytes))
})
.collect();
assert!(
matches!(
Group::new(String::new(), too_many),
Err(Error::VaultGroupTooManyChildren { .. })
),
"the list travels in one node, so it is bounded by MAX_FIELD_LEN"
);
}
#[test]
fn the_child_limit_matches_the_node_it_travels_in() {
assert_eq!(Group::MAX_CHILDREN, 3854);
let per_child = 1 + GroupId::LEN;
assert!(5 + Group::MAX_CHILDREN * per_child <= crate::vault::Entry::MAX_FIELD_LEN);
assert!(5 + (Group::MAX_CHILDREN + 1) * per_child > crate::vault::Entry::MAX_FIELD_LEN);
}
}