115 lines
3.6 KiB
Rust
115 lines
3.6 KiB
Rust
use std::fs;
|
|
use std::path::Path;
|
|
|
|
use serde::de::DeserializeOwned;
|
|
use serde::Serialize;
|
|
use shared::AssetSourceFingerprint;
|
|
|
|
pub(crate) fn fingerprint_file(path: impl AsRef<Path>) -> Result<AssetSourceFingerprint, String> {
|
|
let path = path.as_ref();
|
|
let bytes = fs::read(path)
|
|
.map_err(|error| format!("could not read imported source {}: {error}", path.display()))?;
|
|
Ok(AssetSourceFingerprint::from_bytes(&bytes))
|
|
}
|
|
|
|
/// Writes canonical pretty RON only when the parsed document changes semantically.
|
|
///
|
|
/// Equivalent existing bytes, including custom formatting and final-newline policy, stay intact.
|
|
pub(crate) fn write_pretty_ron_if_changed<T>(
|
|
path: impl AsRef<Path>,
|
|
value: &T,
|
|
) -> Result<bool, String>
|
|
where
|
|
T: DeserializeOwned + PartialEq + Serialize,
|
|
{
|
|
let path = path.as_ref();
|
|
if fs::read_to_string(path)
|
|
.ok()
|
|
.and_then(|text| ron::from_str::<T>(&text).ok())
|
|
.is_some_and(|existing| existing == *value)
|
|
{
|
|
return Ok(false);
|
|
}
|
|
|
|
let text = ron::ser::to_string_pretty(value, ron::ser::PrettyConfig::default())
|
|
.map_err(|error| format!("could not serialize RON: {error}"))?;
|
|
if fs::read(path).ok().as_deref() == Some(text.as_bytes()) {
|
|
return Ok(false);
|
|
}
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)
|
|
.map_err(|error| format!("could not create {}: {error}", parent.display()))?;
|
|
}
|
|
fs::write(path, text)
|
|
.map_err(|error| format!("could not write {}: {error}", path.display()))?;
|
|
Ok(true)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde::Deserialize;
|
|
use std::fs::{File, FileTimes};
|
|
use std::time::{Duration, SystemTime};
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
|
struct Fixture {
|
|
count: u32,
|
|
label: String,
|
|
}
|
|
|
|
fn fixture_path(name: &str) -> std::path::PathBuf {
|
|
std::env::temp_dir().join(format!(
|
|
"blacksite-fingerprint-{name}-{}.ron",
|
|
Uuid::new_v4()
|
|
))
|
|
}
|
|
|
|
#[test]
|
|
fn metadata_only_drift_does_not_change_content_identity() {
|
|
let path = fixture_path("mtime");
|
|
fs::write(&path, b"stable source bytes").unwrap();
|
|
let before = fingerprint_file(&path).unwrap();
|
|
File::options()
|
|
.write(true)
|
|
.open(&path)
|
|
.unwrap()
|
|
.set_times(
|
|
FileTimes::new().set_modified(SystemTime::now() + Duration::from_secs(86_400)),
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(fingerprint_file(&path).unwrap(), before);
|
|
fs::remove_file(path).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn same_size_byte_change_updates_content_identity() {
|
|
let path = fixture_path("same-size");
|
|
fs::write(&path, b"source-a").unwrap();
|
|
let before = fingerprint_file(&path).unwrap();
|
|
fs::write(&path, b"source-b").unwrap();
|
|
let after = fingerprint_file(&path).unwrap();
|
|
|
|
assert_eq!(before.byte_len, after.byte_len);
|
|
assert_ne!(before.content_hash, after.content_hash);
|
|
fs::remove_file(path).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn equivalent_ron_preserves_exact_existing_bytes() {
|
|
let path = fixture_path("semantic");
|
|
let existing = b"( label: \"stable\", count: 7, )\n\n";
|
|
fs::write(&path, existing).unwrap();
|
|
let value = Fixture {
|
|
count: 7,
|
|
label: "stable".into(),
|
|
};
|
|
|
|
assert!(!write_pretty_ron_if_changed(&path, &value).unwrap());
|
|
assert_eq!(fs::read(&path).unwrap(), existing);
|
|
fs::remove_file(path).unwrap();
|
|
}
|
|
}
|