mod reader;
mod storage;
mod writer;
pub use reader::CdxReader;
pub use storage::{ArchiveStorage, MemoryStorage};
pub use writer::{CdxWriter, CompressionMethod};
pub const MANIFEST_PATH: &str = "manifest.json";
pub const CONTENT_PATH: &str = "content/document.json";
pub const DUBLIN_CORE_PATH: &str = "metadata/dublin-core.json";
pub const SIGNATURES_PATH: &str = "security/signatures.json";
pub const ENCRYPTION_PATH: &str = "security/encryption.json";
pub const PHANTOMS_PATH: &str = "phantoms/clusters.json";
pub const ACADEMIC_NUMBERING_PATH: &str = "academic/numbering.json";
pub const COMMENTS_PATH: &str = "collaboration/comments.json";
pub const FORMS_DATA_PATH: &str = "forms/data.json";
pub const BIBLIOGRAPHY_PATH: &str = "semantic/bibliography.json";
pub const JSONLD_PATH: &str = "metadata/jsonld.json";
pub const ZIP_COMMENT: &str = "Codex Document Format v0.1";
#[must_use]
pub fn is_url_safe_path(path: &str) -> bool {
path.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-' | b'_' | b'/'))
}
pub(crate) fn validate_path(path: &str) -> crate::Result<()> {
if path.split('/').any(|component| component == "..") {
return Err(crate::Error::PathTraversal {
path: path.to_string(),
});
}
if path.starts_with('/') {
return Err(crate::Error::PathTraversal {
path: path.to_string(),
});
}
if path.contains('\\') {
return Err(crate::Error::PathTraversal {
path: path.to_string(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_path_safe() {
assert!(validate_path("manifest.json").is_ok());
assert!(validate_path("content/document.json").is_ok());
assert!(validate_path("assets/images/photo.png").is_ok());
}
#[test]
fn test_validate_path_traversal() {
assert!(validate_path("../secret").is_err());
assert!(validate_path("foo/../bar").is_err());
assert!(validate_path("foo/..").is_err());
}
#[test]
fn test_validate_path_absolute() {
assert!(validate_path("/etc/passwd").is_err());
assert!(validate_path("/manifest.json").is_err());
}
#[test]
fn test_validate_path_backslash() {
assert!(validate_path("foo\\bar").is_err());
assert!(validate_path("..\\secret").is_err());
}
#[test]
fn test_url_safe_path_valid() {
assert!(is_url_safe_path("assets/image-01.png"));
assert!(is_url_safe_path("assets/photo_2024.jpg"));
assert!(is_url_safe_path("content/document.json"));
assert!(is_url_safe_path("a-z_0-9/file.ext"));
}
#[test]
fn test_url_safe_path_invalid() {
assert!(!is_url_safe_path("assets/file name.png"));
assert!(!is_url_safe_path("assets/file%20name.png"));
assert!(!is_url_safe_path("assets/文档.txt"));
assert!(!is_url_safe_path("assets/file@2x.png"));
assert!(!is_url_safe_path("assets/file#1.png"));
assert!(!is_url_safe_path("assets/file(1).png"));
}
}