use crate::hex::hex_encode_bytes;
use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
const SERVER_GENERATED_ID_BODY_LEN: usize = 32;
macro_rules! validation_error {
($name:ident, $message:literal) => {
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error($message)]
pub struct $name {
value: String,
reason: String,
}
impl $name {
pub fn value(&self) -> &str {
&self.value
}
pub fn reason(&self) -> &str {
&self.reason
}
}
};
}
validation_error!(
NamespaceIdValidationError,
"invalid namespace_id {value:?}: {reason}"
);
validation_error!(
CommitIdValidationError,
"invalid commit_id {value:?}: {reason}"
);
validation_error!(
GeneratedIdValidationError,
"invalid generated id {value:?}: {reason}"
);
validation_error!(
NameKeyValidationError,
"invalid name_key {value:?}: {reason}"
);
macro_rules! string_id {
(
$(#[$meta:meta])*
$name:ident,
error = $error:ty,
validate = $validate:expr
$(, schema($($schema:tt)+))?
$(,)?
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(
feature = "openapi",
schema(value_type = String $(, $($schema)+)?)
)]
pub struct $name(String);
impl $name {
pub fn parse(value: impl AsRef<str>) -> Result<Self, $error> {
let value = value.as_ref();
($validate)(value)?;
Ok(Self(value.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<&str> for $name {
type Error = $error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl TryFrom<String> for $name {
type Error = $error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl std::str::FromStr for $name {
type Err = $error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::borrow::Borrow<str> for $name {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl serde::Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
Self::parse(value).map_err(serde::de::Error::custom)
}
}
};
(
$(#[$meta:meta])*
$name:ident,
prefix = $prefix:literal
$(, schema($($schema:tt)+))?
$(,)?
) => {
string_id! {
$(#[$meta])*
$name,
error = GeneratedIdValidationError,
validate = |value: &str| validate_generated_id($prefix, value)
$(, schema($($schema)+))?
}
impl $name {
pub fn generate() -> Self {
Self(generated_id($prefix))
}
}
};
}
macro_rules! numeric_id {
(
$(#[$meta:meta])*
$name:ident,
public_ordinal,
schema_description = $schema_description:literal
) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
pub struct $name(pub u64);
impl $name {
pub fn parse(value: u64) -> Result<Self, $crate::PublicOrdinalRangeError> {
if value > $crate::MAX_PUBLIC_INTEGER {
return Err($crate::PublicOrdinalRangeError);
}
Ok(Self(value))
}
}
#[cfg(feature = "openapi")]
impl utoipa::PartialSchema for $name {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
utoipa::openapi::schema::Object::builder()
.schema_type(utoipa::openapi::schema::Type::Integer)
.format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int64,
)))
.minimum(Some(0u64))
.maximum(Some($crate::MAX_PUBLIC_INTEGER))
.description(Some($schema_description))
.into()
}
}
#[cfg(feature = "openapi")]
impl utoipa::ToSchema for $name {}
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <u64 as serde::Deserialize>::deserialize(deserializer)?;
Self::parse(value).map_err(serde::de::Error::custom)
}
}
impl From<u64> for $name {
fn from(value: u64) -> Self {
Self(value)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
};
(
$(#[$meta:meta])*
$name:ident
) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "openapi", schema(value_type = u64))]
pub struct $name(pub u64);
impl From<u64> for $name {
fn from(value: u64) -> Self {
Self(value)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
};
}
pub(crate) use numeric_id;
pub(crate) use string_id;
pub(crate) use validation_error;
pub fn generated_id(prefix: &'static str) -> String {
format!("{prefix}_{}", hex_encode_bytes(&random_128()))
}
fn generated_position_suffix() -> String {
let suffix = hex_encode_bytes(&random_128());
suffix[..16].to_owned()
}
fn random_128() -> [u8; 16] {
let mut bytes = [0_u8; 16];
getrandom::fill(&mut bytes).expect("the system random generator must be available");
bytes
}
fn validate_generated_id(
prefix: &'static str,
value: &str,
) -> Result<(), GeneratedIdValidationError> {
let expected_prefix = format!("{prefix}_");
let Some(body) = value.strip_prefix(&expected_prefix) else {
return Err(generated_id_error(
value,
format!("must start with `{expected_prefix}`"),
));
};
if body.len() != SERVER_GENERATED_ID_BODY_LEN {
return Err(generated_id_error(
value,
format!("body must be {SERVER_GENERATED_ID_BODY_LEN} lowercase hex characters"),
));
}
if !body.bytes().all(is_lower_hex_byte) {
return Err(generated_id_error(
value,
"body must contain only lowercase hex characters".to_owned(),
));
}
Ok(())
}
fn validate_namespace_id(value: &str) -> Result<(), NamespaceIdValidationError> {
validate_id_grammar(value).map_err(|reason| namespace_id_error(value, reason))?;
if value.starts_with("loonfs-") {
return Err(namespace_id_error(
value,
"the `loonfs-` prefix is reserved for LoonFS system namespaces",
));
}
Ok(())
}
fn validate_commit_id(value: &str) -> Result<(), CommitIdValidationError> {
validate_id_grammar(value).map_err(|reason| commit_id_error(value, reason))
}
pub const MAX_NAME_KEY_BYTES: usize = 768;
pub const MAX_ID_BYTES: usize = 128;
fn validate_name_key(value: &str) -> Result<(), NameKeyValidationError> {
if value.is_empty() {
return Err(name_key_error(value, "must not be empty"));
}
if value.contains('/') {
return Err(name_key_error(value, "must not contain `/`"));
}
if matches!(value, "." | "..") {
return Err(name_key_error(value, "must not be `.` or `..`"));
}
if value.chars().any(|character| character.is_control()) {
return Err(name_key_error(value, "must not contain control characters"));
}
if value.len() > MAX_NAME_KEY_BYTES {
return Err(name_key_error(
"",
format!("exceeds the maximum name key length of {MAX_NAME_KEY_BYTES} bytes"),
));
}
Ok(())
}
fn validate_position_suffix_id(
value: &str,
position_label: (&str, &str),
) -> Result<(), GeneratedIdValidationError> {
let Some((position, suffix)) = value.split_once('-') else {
return Err(generated_id_error(
value,
format!(
"must be `<20 digit {}>-<16 lowercase hex>`",
position_label.0
),
));
};
if position.len() != 20 || !position.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(generated_id_error(
value,
format!("{} prefix must be 20 decimal digits", position_label.1),
));
}
if suffix.len() != 16 || !suffix.bytes().all(is_lower_hex_byte) {
return Err(generated_id_error(
value,
"suffix must be 16 lowercase hex characters".to_owned(),
));
}
Ok(())
}
fn is_lower_hex_byte(byte: u8) -> bool {
byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)
}
fn validate_id_grammar(value: &str) -> Result<(), String> {
if value.is_empty() {
return Err("must not be empty".to_owned());
}
if value.len() > MAX_ID_BYTES {
return Err(format!("must be {MAX_ID_BYTES} bytes or fewer"));
}
if value.trim() != value {
return Err("must not have leading or trailing whitespace".to_owned());
}
if matches!(value, "." | "..") {
return Err("must not be `.` or `..`".to_owned());
}
let mut chars = value.chars();
let first = chars
.next()
.expect("empty id returned before char validation");
if !first.is_ascii_lowercase() && !first.is_ascii_digit() {
return Err("must start with a lowercase ASCII letter or digit".to_owned());
}
if !chars.all(is_allowed_id_tail_char) {
return Err(
"must contain only lowercase ASCII letters, digits, `.`, `_`, or `-`".to_owned(),
);
}
Ok(())
}
fn is_allowed_id_tail_char(ch: char) -> bool {
ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '.' | '_' | '-')
}
fn namespace_id_error(value: &str, reason: impl Into<String>) -> NamespaceIdValidationError {
NamespaceIdValidationError {
value: value.to_owned(),
reason: reason.into(),
}
}
fn commit_id_error(value: &str, reason: impl Into<String>) -> CommitIdValidationError {
CommitIdValidationError {
value: value.to_owned(),
reason: reason.into(),
}
}
fn generated_id_error(value: &str, reason: String) -> GeneratedIdValidationError {
GeneratedIdValidationError {
value: value.to_owned(),
reason,
}
}
fn name_key_error(value: &str, reason: impl Into<String>) -> NameKeyValidationError {
NameKeyValidationError {
value: value.to_owned(),
reason: reason.into(),
}
}
string_id! {
NamespaceId,
error = NamespaceIdValidationError,
validate = validate_namespace_id,
schema(
pattern = r"^[a-z0-9][a-z0-9._-]{0,127}$",
example = "demo"
)
}
string_id! {
ContentStoreId,
prefix = "cs"
}
string_id! {
CommitId,
error = CommitIdValidationError,
validate = validate_commit_id,
schema(
pattern = r"^[a-z0-9][a-z0-9._-]{0,127}$",
example = "c_f3a9c2d4b6e8417a90c5d2f8e1b7a6c0"
)
}
impl CommitId {
pub fn generate() -> Self {
Self(generated_id("c"))
}
}
string_id! {
CheckpointId,
prefix = "chk",
schema(
pattern = r"^chk_[0-9a-f]{32}$",
example = "chk_00000000000000000000000000000002"
)
}
string_id! {
UploadId,
prefix = "upl",
schema(
pattern = r"^upl_[0-9a-f]{32}$",
example = "upl_4d8f2c91a7b34e0f9c6d1a2b3e5f708c"
)
}
string_id! {
ContentId,
error = GeneratedIdValidationError,
validate = |value: &str| validate_generated_id("con", value),
schema(
pattern = r"^con_[0-9a-f]{32}$",
example = "con_9f2a6c0e4b7d4a90b13f0d8c5e6a2b41"
)
}
impl ContentId {
pub fn generate() -> Self {
Self(format!(
"con_{}",
crate::hex::hex_encode_bytes(&random_128())
))
}
pub fn shard_prefixes(&self) -> [&str; CONTENT_ID_SHARD_LEVELS] {
let first_start = CONTENT_ID_PREFIX_LEN;
let second_start = first_start + CONTENT_ID_SHARD_WIDTH;
[
&self.0[first_start..second_start],
&self.0[second_start..second_start + CONTENT_ID_SHARD_WIDTH],
]
}
}
const CONTENT_ID_PREFIX_LEN: usize = "con_".len();
const CONTENT_ID_SHARD_LEVELS: usize = 2;
const CONTENT_ID_SHARD_WIDTH: usize = 2;
string_id! {
MetadataTableId,
prefix = "tbl"
}
string_id! {
MetadataCompactionId,
prefix = "cmp"
}
string_id! {
IndexSegmentId,
prefix = "idx"
}
string_id! {
GrepManifestId,
prefix = "gmf"
}
string_id! {
ManifestObjectId,
error = GeneratedIdValidationError,
validate = |value| {
validate_position_suffix_id(value, ("manifest_id", "manifest id"))
}
}
impl ManifestObjectId {
pub fn generate(manifest_id: ManifestId) -> Self {
Self(format!(
"{:020}-{}",
manifest_id.0,
generated_position_suffix()
))
}
}
pub fn manifest_object_id_manifest_id(object_id: &str) -> Option<ManifestId> {
validate_position_suffix_id(object_id, ("manifest_id", "manifest id")).ok()?;
let (position, _) = object_id.split_once('-')?;
position
.parse()
.ok()
.and_then(|value| ManifestId::parse(value).ok())
}
string_id! {
WalSegmentId,
error = GeneratedIdValidationError,
validate = |value| {
validate_position_suffix_id(value, ("start_seq", "position"))
}
}
impl WalSegmentId {
pub fn generate(start_seq: ChangeSeq) -> Self {
Self(format!(
"{:020}-{}",
start_seq.0,
generated_position_suffix()
))
}
}
pub fn wal_segment_id_start_seq(segment_id: &str) -> Option<ChangeSeq> {
validate_position_suffix_id(segment_id, ("start_seq", "position")).ok()?;
let (position, _) = segment_id.split_once('-')?;
position
.parse()
.ok()
.and_then(|value| ChangeSeq::parse(value).ok())
}
string_id! {
NameKey,
error = NameKeyValidationError,
validate = validate_name_key,
schema(example = "report.txt")
}
impl NameKey {
pub fn for_display_name(display_name: &crate::DisplayName) -> Self {
Self(crate::name_key_for_display_name(display_name.as_str()))
}
}
pub const MAX_PUBLIC_INTEGER: u64 = 9_007_199_254_740_991;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PublicOrdinalRangeError;
impl fmt::Display for PublicOrdinalRangeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "must be an integer from 0 through {MAX_PUBLIC_INTEGER}")
}
}
impl std::error::Error for PublicOrdinalRangeError {}
pub fn next_public_ordinal(current: u64) -> Option<u64> {
current
.checked_add(1)
.filter(|next| *next <= MAX_PUBLIC_INTEGER)
}
numeric_id! {
InodeId
}
pub const ROOT_INODE_ID: InodeId = InodeId(1);
pub const FIRST_ALLOCATABLE_INODE_ID: InodeId = InodeId(ROOT_INODE_ID.0 + 1);
numeric_id! {
RevisionNo,
public_ordinal,
schema_description = "Revision number for a file's content. It increases whenever the content is replaced or restored."
}
numeric_id! {
ChangeSeq,
public_ordinal,
schema_description = "Sequence number assigned to a namespace commit. It determines the order in which commits become visible."
}
numeric_id! {
ManifestId,
public_ordinal,
schema_description = "Version number for a namespace manifest. It can increase when metadata changes, even if no namespace commit is written."
}
numeric_id! {
WriterEpoch,
public_ordinal,
schema_description = "Counter used to reject writes from an older writer."
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum InodeKind {
File,
#[serde(rename = "dir")]
Directory,
}
impl fmt::Display for InodeKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::File => f.write_str("file"),
Self::Directory => f.write_str("dir"),
}
}
}
#[cfg(test)]
mod tests {
use super::{
next_public_ordinal, ChangeSeq, CheckpointId, CommitId, ContentId, ContentStoreId, InodeId,
ManifestId, ManifestObjectId, MetadataTableId, NameKey, NamespaceId, RevisionNo, UploadId,
WalSegmentId, WriterEpoch, MAX_PUBLIC_INTEGER,
};
use crate::AttributeRevisionNo;
use std::collections::BTreeSet;
#[test]
fn public_ordinal_advancement_accepts_the_maximum_and_rejects_the_next_value() {
assert_eq!(
next_public_ordinal(MAX_PUBLIC_INTEGER - 1),
Some(MAX_PUBLIC_INTEGER)
);
assert_eq!(next_public_ordinal(MAX_PUBLIC_INTEGER), None);
}
#[test]
fn public_ordinal_inputs_must_fit_the_json_safe_integer_range() {
macro_rules! assert_range {
($type:ty) => {{
let constructed = <$type>::parse(MAX_PUBLIC_INTEGER)
.expect("construct the maximum public ordinal");
assert_eq!(constructed.0, MAX_PUBLIC_INTEGER);
let construction_error = <$type>::parse(MAX_PUBLIC_INTEGER + 1)
.expect_err("reject a value above the public limit");
assert_eq!(
construction_error.to_string(),
"must be an integer from 0 through 9007199254740991"
);
let maximum = serde_json::from_str::<$type>(&MAX_PUBLIC_INTEGER.to_string())
.expect("deserialize the maximum public ordinal");
assert_eq!(maximum.0, MAX_PUBLIC_INTEGER);
let error = serde_json::from_str::<$type>(&(MAX_PUBLIC_INTEGER + 1).to_string())
.expect_err("ordinal above the public range");
assert!(
error
.to_string()
.contains("must be an integer from 0 through 9007199254740991"),
"unexpected range error: {error}"
);
}};
}
assert_range!(RevisionNo);
assert_range!(ChangeSeq);
assert_range!(AttributeRevisionNo);
assert_range!(ManifestId);
assert_range!(WriterEpoch);
assert_eq!(
serde_json::from_str::<InodeId>(&(MAX_PUBLIC_INTEGER + 1).to_string())
.expect("inode ids retain the full u64 range"),
InodeId(MAX_PUBLIC_INTEGER + 1)
);
}
#[test]
fn namespace_id_parse_accepts_allowed_grammar() {
let long_id = format!("a{}", "b".repeat(127));
for value in ["demo", "demo-1", "demo_1", "demo.v1", &long_id] {
let parsed = NamespaceId::parse(value).expect("valid namespace_id");
assert_eq!(parsed.as_str(), value);
}
}
#[test]
fn namespace_id_parse_rejects_invalid_values() {
let long_id = format!("a{}", "b".repeat(128));
for value in [
"", "/", "a/b", ".", "..", " demo", "demo ", "demo\n", "demo?", "demo#", "demo%",
"Demo", &long_id,
] {
assert!(
NamespaceId::parse(value).is_err(),
"expected invalid namespace_id {value:?}"
);
}
}
#[test]
fn namespace_id_parse_rejects_reserved_system_prefix() {
assert!(NamespaceId::parse("loonfs-doctor-abc").is_err());
assert!(NamespaceId::parse("loonfs-").is_err());
assert_eq!(
NamespaceId::parse("my-loonfs-notes")
.expect("non-prefixed use is allowed")
.as_str(),
"my-loonfs-notes"
);
assert!(CommitId::parse("loonfs-retry-1").is_ok());
}
#[test]
fn commit_id_parse_uses_same_allowed_grammar() {
let parsed = CommitId::parse("c_demo-1").expect("valid commit_id");
assert_eq!(parsed.as_str(), "c_demo-1");
assert!(CommitId::parse("c/demo").is_err());
assert!(CommitId::parse("C_demo").is_err());
}
#[test]
fn identity_try_from_validates_values() {
assert_eq!(
NamespaceId::try_from("demo")
.expect("valid namespace id")
.as_str(),
"demo"
);
assert_eq!(
CommitId::try_from("commit-1")
.expect("valid commit id")
.as_str(),
"commit-1"
);
assert_eq!(
ContentStoreId::try_from("cs_00000000000000000000000000000001")
.expect("valid content store id")
.as_str(),
"cs_00000000000000000000000000000001"
);
assert_eq!(
CheckpointId::try_from("chk_00000000000000000000000000000001")
.expect("valid checkpoint id")
.as_str(),
"chk_00000000000000000000000000000001"
);
assert_eq!(
NameKey::try_from("report.txt".to_owned())
.expect("valid name key")
.as_str(),
"report.txt"
);
assert_eq!(
ManifestObjectId::try_from("00000000000000000042-0123456789abcdef")
.expect("valid manifest object id")
.as_str(),
"00000000000000000042-0123456789abcdef"
);
assert!(NamespaceId::try_from("invalid/name").is_err());
assert!(CommitId::try_from("invalid/name").is_err());
assert!(ContentStoreId::try_from("cs_0000000000000000000000000000000g").is_err());
assert!(CheckpointId::try_from("chk_0000000000000000000000000000000g").is_err());
assert!(NameKey::try_from("a/b").is_err());
assert!(ManifestObjectId::try_from("42-0123456789abcdef").is_err());
}
#[test]
fn identity_from_str_delegates_to_parse() {
let namespace_id: NamespaceId = "demo".parse().expect("valid namespace id");
assert_eq!(namespace_id.as_str(), "demo");
assert!("invalid/name".parse::<NamespaceId>().is_err());
}
#[test]
fn identity_deserialize_validates_values() {
let namespace_id: NamespaceId =
serde_json::from_str(r#""demo""#).expect("valid namespace id json");
assert_eq!(namespace_id.as_str(), "demo");
let commit_id: CommitId =
serde_json::from_str(r#""commit-1""#).expect("valid commit id json");
assert_eq!(commit_id.as_str(), "commit-1");
let content_store_id: ContentStoreId =
serde_json::from_str(r#""cs_00000000000000000000000000000001""#)
.expect("valid content store id json");
assert_eq!(
content_store_id.as_str(),
"cs_00000000000000000000000000000001"
);
let checkpoint_id: CheckpointId =
serde_json::from_str(r#""chk_00000000000000000000000000000001""#)
.expect("valid checkpoint id json");
assert_eq!(
checkpoint_id.as_str(),
"chk_00000000000000000000000000000001"
);
let namespace_error = serde_json::from_str::<NamespaceId>(r#""invalid/name""#)
.expect_err("invalid namespace id json");
assert!(namespace_error.to_string().contains("namespace_id"));
let commit_error = serde_json::from_str::<CommitId>(r#""invalid/name""#)
.expect_err("invalid commit id json");
assert!(commit_error.to_string().contains("commit_id"));
let content_store_error =
serde_json::from_str::<ContentStoreId>(r#""cs_0000000000000000000000000000000g""#)
.expect_err("invalid content store id json");
assert!(content_store_error.to_string().contains("generated id"));
let checkpoint_error =
serde_json::from_str::<CheckpointId>(r#""chk_0000000000000000000000000000000g""#)
.expect_err("invalid checkpoint id json");
assert!(checkpoint_error.to_string().contains("generated id"));
}
#[test]
fn generated_content_store_id_parse_requires_prefix_and_lower_hex_body() {
let parsed = ContentStoreId::parse("cs_00000000000000000000000000000001")
.expect("valid content store id");
assert_eq!(parsed.as_str(), "cs_00000000000000000000000000000001");
let hyphenated_content_store_id = ["cs", "1"].join("-");
for value in [
hyphenated_content_store_id.as_str(),
"upl_00000000000000000000000000000001",
"content-stores/foo",
"cs_",
"cs_abcdef",
"cs_0000000000000000000000000000000",
"cs_000000000000000000000000000000001",
"cs_ABCDEF00000000000000000000000000",
"cs_0000000000000000000000000000000g",
" cs_00000000000000000000000000000001",
"cs_00000000000000000000000000000001 ",
] {
assert!(
ContentStoreId::parse(value).is_err(),
"expected invalid content store id {value:?}"
);
}
}
#[test]
fn generated_upload_wal_segment_table_and_checkpoint_ids_reject_hyphenated_ids() {
assert!(UploadId::parse("upl_00000000000000000000000000000001").is_ok());
assert!(MetadataTableId::parse("tbl_00000000000000000000000000000001").is_ok());
assert!(CheckpointId::parse("chk_00000000000000000000000000000001").is_ok());
assert!(UploadId::parse(["upl", "123"].join("-")).is_err());
assert!(WalSegmentId::parse("00000000000000000412-9f2a6c0e4b7d4a90").is_ok());
assert!(ManifestObjectId::parse("00000000000000000412-9f2a6c0e4b7d4a90").is_ok());
assert!(WalSegmentId::parse("412-9f2a6c0e4b7d4a90").is_err());
assert!(WalSegmentId::parse("00000000000000000412-9F2A6C0E4B7D4A90").is_err());
assert!(ManifestObjectId::parse("412-9f2a6c0e4b7d4a90").is_err());
assert!(ManifestObjectId::parse("00000000000000000412-9F2A6C0E4B7D4A90").is_err());
assert!(ManifestObjectId::parse("mf_9f2a6c0e4b7d4a90b13f0d8c5e6a2b41").is_err());
assert!(WalSegmentId::parse("seg_9f2a6c0e4b7d4a90b13f0d8c5e6a2b41").is_err());
assert!(MetadataTableId::parse(["tbl", "123"].join("-")).is_err());
assert!(CheckpointId::parse(["chk", "123"].join("-")).is_err());
}
#[test]
fn generated_runtime_ids_use_lower_hex_bodies() {
let upload_id = UploadId::generate();
let wal_segment_id = WalSegmentId::generate(ChangeSeq(412));
let manifest_object_id = ManifestObjectId::generate(ManifestId(413));
let metadata_table_id = MetadataTableId::generate();
let checkpoint_id = CheckpointId::generate();
assert_generated_id_shape(upload_id.as_str(), "upl");
assert!(wal_segment_id.as_str().starts_with("00000000000000000412-"));
assert!(manifest_object_id
.as_str()
.starts_with("00000000000000000413-"));
assert_generated_id_shape(metadata_table_id.as_str(), "tbl");
assert_generated_id_shape(checkpoint_id.as_str(), "chk");
assert!(UploadId::parse(upload_id.as_str()).is_ok());
assert!(WalSegmentId::parse(wal_segment_id.as_str()).is_ok());
assert!(ManifestObjectId::parse(manifest_object_id.as_str()).is_ok());
assert!(MetadataTableId::parse(metadata_table_id.as_str()).is_ok());
assert!(CheckpointId::parse(checkpoint_id.as_str()).is_ok());
}
#[test]
fn generated_wal_segment_ids_are_not_reused_across_samples() {
let mut ids = BTreeSet::new();
for _ in 0..128 {
let id = WalSegmentId::generate(ChangeSeq(412));
assert!(
ids.insert(id.clone()),
"generated duplicate WAL segment id {id}"
);
}
}
#[test]
fn generated_manifest_object_ids_are_not_reused_across_samples() {
let mut ids = BTreeSet::new();
for _ in 0..128 {
let id = ManifestObjectId::generate(ManifestId(412));
assert!(
ids.insert(id.clone()),
"generated duplicate manifest object id {id}"
);
}
}
#[test]
fn wal_segment_id_start_seq_reads_position_prefix() {
assert_eq!(
super::wal_segment_id_start_seq("00000000000000000412-9f2a6c0e4b7d4a90"),
Some(ChangeSeq(412))
);
assert_eq!(super::wal_segment_id_start_seq("not-a-segment-id"), None);
assert_eq!(
super::wal_segment_id_start_seq("00009007199254740992-9f2a6c0e4b7d4a90"),
None
);
}
#[test]
fn manifest_object_id_manifest_id_reads_position_prefix() {
assert_eq!(
super::manifest_object_id_manifest_id("00000000000000000412-9f2a6c0e4b7d4a90"),
Some(ManifestId(412))
);
assert_eq!(
super::manifest_object_id_manifest_id("not-a-manifest-object-id"),
None
);
assert_eq!(
super::manifest_object_id_manifest_id("00009007199254740992-9f2a6c0e4b7d4a90"),
None
);
}
#[test]
fn generated_content_ids_are_unique_and_shard_uniformly() {
let mut ids = BTreeSet::new();
let mut first_level_shards = BTreeSet::new();
let mut leaf_shards = BTreeSet::new();
for _ in 0..512 {
let id = ContentId::generate();
assert_generated_id_shape(id.as_str(), "con");
let [first, second] = id.shard_prefixes();
assert_eq!(first, &id.as_str()["con_".len().."con_".len() + 2]);
assert_eq!(second, &id.as_str()["con_".len() + 2.."con_".len() + 4]);
first_level_shards.insert(first.to_owned());
leaf_shards.insert(format!("{first}/{second}"));
assert!(
ids.insert(id.clone()),
"generated duplicate content id {id}"
);
}
assert!(
first_level_shards.len() > 128,
"content id first-level shards are not spread: {} distinct",
first_level_shards.len()
);
assert!(
leaf_shards.len() > 480,
"content id leaf shards are not spread: {} distinct",
leaf_shards.len()
);
}
#[test]
fn content_id_parse_requires_the_generated_id_shape() {
assert!(ContentId::parse("con_0123456789abcdef0123456789abcdef").is_ok());
for value in [
"con_",
"con_abcdef",
"con_0123456789ABCDEF0123456789abcdef",
"con_0123456789abcdef0123456789abcde",
"upl_0123456789abcdef0123456789abcdef",
"0123456789abcdef0123456789abcdef",
] {
assert!(
ContentId::parse(value).is_err(),
"expected invalid content id {value:?}"
);
}
}
fn assert_generated_id_shape(value: &str, prefix: &str) {
let expected_prefix = format!("{prefix}_");
let body = value
.strip_prefix(&expected_prefix)
.expect("generated id prefix");
assert_eq!(body.len(), 32);
assert!(
body.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)),
"generated id body must be lowercase hex: {value}"
);
}
#[test]
fn name_key_parse_rejects_invalid_values() {
assert_eq!(
NameKey::parse("").expect_err("empty").reason(),
"must not be empty"
);
assert_eq!(
NameKey::parse("a/b").expect_err("slash").reason(),
"must not contain `/`"
);
assert_eq!(
NameKey::parse(".").expect_err("dot").reason(),
"must not be `.` or `..`"
);
assert_eq!(
NameKey::parse("a\u{0}b").expect_err("control").reason(),
"must not contain control characters"
);
NameKey::parse("k".repeat(super::MAX_NAME_KEY_BYTES)).expect("cap is inclusive");
assert_eq!(
NameKey::parse("k".repeat(super::MAX_NAME_KEY_BYTES + 1))
.expect_err("over cap")
.reason(),
"exceeds the maximum name key length of 768 bytes"
);
}
#[test]
fn name_key_serializes_as_string_and_validates_deserialize() {
let name_key = NameKey::parse("report.txt").expect("valid name key");
assert_eq!(
serde_json::to_string(&name_key).expect("serialize name key"),
"\"report.txt\""
);
assert_eq!(
serde_json::from_str::<NameKey>("\"report.txt\"").expect("deserialize name key"),
name_key
);
assert!(serde_json::from_str::<NameKey>("\"a/b\"").is_err());
}
}