use snafu::prelude::*;
use crate::{
Result,
config::{
VALIDATION_MARKDOWN_MAX_LEN, VALIDATION_MAX_QUERY_LEN, VALIDATION_NAME_MAX_LEN,
VALIDATION_OID_MAX_LEN, VALIDATION_OID_MIN_LEN, VALIDATION_TAG_MAX_COUNT,
VALIDATION_TAG_MAX_LEN,
},
prelude::*,
};
fn is_cid_chars(s: &str) -> bool {
s.bytes().all(|b| matches!(b, b'a'..=b'z' | b'2'..=b'7'))
}
fn is_base36_chars(s: &str) -> bool {
s.bytes().all(|b| matches!(b, b'a'..=b'z' | b'0'..=b'9'))
}
pub fn looks_like_object_id(s: &str) -> bool {
const PREFIX: &str = "bafyrei";
const LEN: usize = 59;
if s.len() < LEN || !s.starts_with(PREFIX) {
return false;
}
if s.len() == LEN && is_cid_chars(s) {
return true;
}
if let Some((p1, p2)) = s.split_once('.')
&& p1.len() == LEN
&& is_cid_chars(p1)
&& matches!(p2.len(), 1..=13)
&& is_base36_chars(p2)
{
return true;
}
false
}
#[derive(Debug, Clone)]
pub struct ValidationLimits {
pub markdown_max_len: u64,
pub name_max_len: u64,
pub tag_max_count: u64,
pub tag_max_len: u64,
pub oid_min_len: u64,
pub oid_max_len: u64,
pub max_query_len: u64,
}
impl Default for ValidationLimits {
fn default() -> Self {
ValidationLimits {
markdown_max_len: VALIDATION_MARKDOWN_MAX_LEN,
name_max_len: VALIDATION_NAME_MAX_LEN,
tag_max_count: VALIDATION_TAG_MAX_COUNT,
tag_max_len: VALIDATION_TAG_MAX_LEN,
oid_min_len: VALIDATION_OID_MIN_LEN,
oid_max_len: VALIDATION_OID_MAX_LEN,
max_query_len: VALIDATION_MAX_QUERY_LEN,
}
}
}
impl ValidationLimits {
#[doc(hidden)]
pub fn validate_id(&self, id: &str, description: &str) -> Result<()> {
ensure!(
!id.is_empty(),
ValidationSnafu {
message: format!("{description} id cannot be empty"),
}
);
ensure!(
looks_like_object_id(id),
ValidationSnafu {
message: format!("{description} not a valid object id",),
}
);
Ok(())
}
#[doc(hidden)]
pub fn validate_name(&self, name: impl Into<String>, description: &str) -> Result<()> {
let name = name.into();
ensure!(
!name.is_empty(),
ValidationSnafu {
message: format!("{description} name cannot be empty"),
}
);
ensure!(
name.len() <= self.name_max_len as usize,
ValidationSnafu {
message: format!(
"{description} name too long: {} bytes (max: {})",
name.len(),
self.name_max_len
),
}
);
Ok(())
}
#[doc(hidden)]
pub fn validate_markdown(&self, md: &str, description: &str) -> Result<()> {
ensure!(
md.len() <= self.markdown_max_len as usize,
ValidationSnafu {
message: format!(
"{description} markdown too long: {} bytes (max: {})",
md.len(),
self.markdown_max_len
),
}
);
Ok(())
}
#[doc(hidden)]
pub fn validate_body(&self, bytes: &bytes::Bytes, description: &str) -> Result<()> {
ensure!(
bytes.len() <= self.markdown_max_len as usize,
ValidationSnafu {
message: format!(
"{description} body too long: {} bytes (max: {})",
bytes.len(),
self.markdown_max_len
),
}
);
Ok(())
}
#[doc(hidden)]
pub fn validate_tag(&self, tag: &str, description: &str) -> Result<()> {
ensure!(
!tag.is_empty(),
ValidationSnafu {
message: format!("{description} tag cannot be an empty string"),
}
);
ensure!(
tag.len() <= self.tag_max_len as usize,
ValidationSnafu {
message: format!(
"{description} tag too long: {} bytes (max: {})",
tag.len(),
self.tag_max_len
),
}
);
Ok(())
}
#[doc(hidden)]
pub fn validate_num_tags(&self, count: usize, description: &str) -> Result<()> {
ensure!(
count <= self.tag_max_count as usize,
ValidationSnafu {
message: format!(
"{description} too many tags: {count} (max: {})",
self.tag_max_count
),
}
);
Ok(())
}
#[doc(hidden)]
pub fn validate_tags(&self, tags: &[String], description: &str) -> Result<()> {
self.validate_num_tags(tags.len(), description)?;
for tag in tags {
self.validate_tag(tag, description)?;
}
Ok(())
}
#[doc(hidden)]
pub fn validate_query(&self, query: &[(String, String)]) -> Result<()> {
let mut query_size = 0;
for (key, val) in query.iter() {
query_size += key.len() + val.len() + 1;
}
ensure!(
query_size <= self.max_query_len as usize,
ValidationSnafu {
message: format!("query too long {query_size}")
}
);
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_validate_name() -> Result<()> {
let limits = ValidationLimits::default();
limits.validate_name("My Object", "")?;
assert!(limits.validate_name("", "test").is_err(), "empty name");
let long = "x".repeat((limits.name_max_len + 1) as usize);
assert!(
limits.validate_name(&long, "test").is_err(),
"too long name"
);
Ok(())
}
#[test]
fn test_validate_tag_name() -> Result<()> {
let limits = ValidationLimits::default();
limits.validate_tag("important", "ok tag")?;
assert!(limits.validate_tag("", "test").is_err(), "empty tag");
let long = "x".repeat((limits.tag_max_len + 1) as usize);
assert!(limits.validate_tag(&long, "test").is_err(), "tag too long");
assert!(
limits
.validate_num_tags((limits.tag_max_count + 1) as usize, "too many")
.is_err(),
"too many tags"
);
Ok(())
}
#[test]
fn test_validate_id() -> Result<()> {
let limits = ValidationLimits::default();
let valid_id = "bafyreie6n5l5nkbjal37su54cha4coy7qzuhrnajluzv5qd5jvtsrxkequ";
limits.validate_id(valid_id, "Object ID")?;
assert!(limits.validate_id("", "Object ID").is_err(), "empty oid");
assert!(
limits.validate_id("short", "Object ID").is_err(),
"oid too short"
);
let long = "x".repeat((limits.oid_max_len + 1) as usize);
assert!(
limits.validate_id(&long, "Object ID").is_err(),
"oid too long"
);
assert!(
limits.validate_id("test\x00id", "Object ID").is_err(),
"oid with invalid chars"
);
Ok(())
}
#[test]
fn test_looks_like_object_id() {
for good_example in [
"bafyreiafl45wf5eaxiby44pxrkhia3y5jsyix3ov2jzqiftsxjotujqlh4",
"bafyreifmrdlvfk5uolhph6xmh6geta47auzqjilcsxarpyxlkrbqxks64a",
] {
assert!(looks_like_object_id(good_example));
}
for bad_example in [
"bafyreiafl45wf5eaxiby44pxrkhia3y5jsyix3ov2jzqiftsxjo", "bafyreiafl45wf5eaxiby44pxrkhia3y5jsyix3ov2jzqiftsxjotujqlh44", "xafyreifmrdlvfk5uolhph6xmh6geta47auzqjilcsxarpyxlkrbqxks64a", "bafyreifmrdlvfk5uolhph6xmh6geta47auzqjilcsxarpyxl0rbqxks64a", ] {
assert!(!looks_like_object_id(bad_example));
}
}
}