use notedthat_core::{KbManifest, KbSlug, ObjectPath, StagedBody};
use crate::WriteError;
pub const MANIFEST_MAX_BYTES: u64 = 1024 * 1024;
pub(crate) fn is_manifest(path: &ObjectPath) -> bool {
path.as_str() == KbManifest::KEY
}
pub(crate) fn check_manifest_bytes(
kb: &KbSlug,
path: &ObjectPath,
bytes: &[u8],
) -> Result<(), WriteError> {
if !is_manifest(path) {
return Ok(());
}
check_size(bytes.len() as u64)?;
validate_manifest_bytes(kb, bytes)
}
pub(crate) async fn check_staged_manifest(
kb: &KbSlug,
path: &ObjectPath,
body: &StagedBody,
) -> Result<(), WriteError> {
if !is_manifest(path) {
return Ok(());
}
check_size(body.len())?;
let bytes = body
.prefix(usize::try_from(body.len()).unwrap_or(usize::MAX))
.await
.map_err(|error| WriteError::InvalidManifest {
message: format!("manifest could not be read back for validation: {error}"),
})?;
validate_manifest_bytes(kb, &bytes)
}
fn check_size(size: u64) -> Result<(), WriteError> {
if size > MANIFEST_MAX_BYTES {
return Err(WriteError::InvalidManifest {
message: format!(
"manifest is {size} bytes; a manifest is at most {MANIFEST_MAX_BYTES}"
),
});
}
Ok(())
}
fn validate_manifest_bytes(kb: &KbSlug, bytes: &[u8]) -> Result<(), WriteError> {
let manifest: KbManifest =
serde_json::from_slice(bytes).map_err(|e| WriteError::InvalidManifest {
message: format!("manifest is not a valid manifest document: {e}"),
})?;
if manifest.kb_slug != *kb {
return Err(WriteError::InvalidManifest {
message: format!(
"manifest names knowledge base '{}' but was written to '{}'",
manifest.kb_slug.as_str(),
kb.as_str()
),
});
}
manifest
.validate()
.map_err(|e| WriteError::InvalidManifest {
message: e.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn kb() -> KbSlug {
KbSlug::try_new("notes").unwrap()
}
fn manifest_path() -> ObjectPath {
ObjectPath::try_from_str(KbManifest::KEY).unwrap()
}
fn manifest(description: &str) -> String {
format!(
r#"{{"notedthat_version":"0.7.2","manifest_version":1,"tenant_slug":"default","kb_slug":"notes","display_name":"Notes","description":"{description}","created_at":1700000000,"access":[]}}"#
)
}
#[test]
fn an_ordinary_key_is_not_checked() {
let path = ObjectPath::try_from_str("notes/a.md").unwrap();
assert!(check_manifest_bytes(&kb(), &path, b"# not json").is_ok());
}
#[test]
fn the_manifest_startup_would_accept_is_accepted() {
let body = manifest("Engineering notes and ADRs.");
assert!(check_manifest_bytes(&kb(), &manifest_path(), body.as_bytes()).is_ok());
}
#[test]
fn a_description_outside_the_limits_is_refused_with_the_boot_message() {
let body = manifest("two\\nlines");
let error = check_manifest_bytes(&kb(), &manifest_path(), body.as_bytes()).unwrap_err();
assert!(
matches!(&error, WriteError::InvalidManifest { message } if message.contains("description")),
"{error}"
);
}
#[test]
fn a_body_that_is_not_a_manifest_is_refused() {
let error = check_manifest_bytes(&kb(), &manifest_path(), b"# not json").unwrap_err();
assert!(
matches!(error, WriteError::InvalidManifest { .. }),
"{error}"
);
}
#[test]
fn a_manifest_naming_another_knowledge_base_is_refused() {
let body = manifest("fine").replace(r#""kb_slug":"notes""#, r#""kb_slug":"other""#);
let error = check_manifest_bytes(&kb(), &manifest_path(), body.as_bytes()).unwrap_err();
assert!(
matches!(&error, WriteError::InvalidManifest { message } if message.contains("'other'")),
"{error}"
);
}
#[test]
fn a_manifest_over_the_size_bound_is_refused_unparsed() {
let body = vec![b' '; usize::try_from(MANIFEST_MAX_BYTES).unwrap() + 1];
let error = check_manifest_bytes(&kb(), &manifest_path(), &body).unwrap_err();
assert!(
matches!(&error, WriteError::InvalidManifest { message } if message.contains("at most")),
"{error}"
);
}
#[tokio::test]
async fn a_staged_manifest_is_read_back_and_checked() {
let good = StagedBody::from(bytes::Bytes::from(manifest("fine")));
assert!(
check_staged_manifest(&kb(), &manifest_path(), &good)
.await
.is_ok()
);
let bad = StagedBody::from(bytes::Bytes::from_static(b"{}"));
assert!(
check_staged_manifest(&kb(), &manifest_path(), &bad)
.await
.is_err()
);
}
}