fix(release): 完成官方发布身份与 legacy CAS 归属保护
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

This commit is contained in:
2026-09-13 15:34:42 +08:00
parent 5bae90cb14
commit 37d49c9793
11 changed files with 416 additions and 67 deletions
+224 -5
View File
@@ -69,6 +69,8 @@ impl std::error::Error for DownloadError {}
const DOWNLOAD_MANIFEST_FILE: &str = "official-download-manifest.json";
const DOWNLOAD_QUARANTINE_FILE: &str = "official-download-quarantine.json";
/// Independent publication fact for the official distribution manifest.
pub const OFFICIAL_DISTRIBUTION_PUBLICATION_FILE: &str = "official-distribution-publication.json";
/// 记录一个已发布官方 release 获取的 CAS 引用。
pub const OFFICIAL_CAS_REUSE_REFERENCES_FILE: &str = "official-cas-reuse-references.json";
const OFFICIAL_CAS_REUSE_REFERENCES_VERSION: u32 = 1;
@@ -553,6 +555,158 @@ fn cas_has_ownership(cas_root: &Path, ownership_id: &str) -> Result<bool, String
})
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct OfficialDistributionPublicationAnchor {
pub(crate) version: u32,
pub(crate) official_release_id: String,
pub(crate) mapping_identity: String,
pub(crate) manifest_identity: String,
pub(crate) entry_count: u64,
}
const OFFICIAL_DISTRIBUTION_PUBLICATION_VERSION: u32 = 1;
/// Writes the independent publication anchor after the complete official
/// release verification has succeeded.
///
/// The caller must invoke this only for a fully audited staging tree immediately
/// before publishing it. The function repeats the manifest/file checks so the
/// anchor cannot be created over an incomplete tree.
pub(crate) fn write_official_distribution_publication_anchor_at(
release_root: &Path,
official_release_id: &str,
) -> Result<(), String> {
if official_release_id.is_empty() {
return Err("官方 distribution publication 缺少 release ID".to_string());
}
ensure_safe_directory_path(release_root, "官方 distribution publication 根目录")?;
let manifest_path = release_root.join(DOWNLOAD_MANIFEST_FILE);
let manifest_bytes = read_file_no_symlink(&manifest_path, "官方下载 manifest")?
.ok_or_else(|| format!("缺少官方下载 manifest{}", manifest_path.display()))?;
let manifest: OfficialDownloadManifest =
serde_json::from_slice(&manifest_bytes).map_err(|error| {
format!(
"解析官方下载 manifest 失败 {}{error}",
manifest_path.display()
)
})?;
if manifest.version != DOWNLOAD_MANIFEST_VERSION {
return Err(format!(
"不支持的官方下载 manifest 版本:{}",
manifest.version
));
}
let mapping_identity = official_distribution_mapping_identity(&manifest);
let expected_index = official_distribution_destination_index(&manifest)?;
if manifest.distribution_mapping_identity.as_deref() != Some(mapping_identity.as_str())
|| manifest.destination_index != expected_index
{
return Err(format!(
"官方下载 manifest 未完成 distribution mapping 验证:{}",
manifest_path.display()
));
}
for entry in manifest.entries.values() {
let path = release_root.join(&entry.destination);
ensure_path_within_root(release_root, &path)?;
ensure_safe_file_target(release_root, &path, "官方 distribution 文件")?;
let bytes = fs::read(&path).map_err(|error| {
format!("读取官方 distribution 文件失败 {}{error}", path.display())
})?;
let actual = blake3::hash(&bytes).to_hex().to_string();
if bytes.len() as u64 != entry.bytes || actual != entry.blake3 {
return Err(format!(
"官方 distribution 文件完整性失败:{}",
entry.destination
));
}
if url_or_path_has_zip_extension(&entry.url) || path_has_zip_extension(&path) {
validate_zip_structure(&path).map_err(|error| {
format!(
"官方 distribution ZIP 结构校验失败 {}{error}",
path.display()
)
})?;
}
}
let anchor = OfficialDistributionPublicationAnchor {
version: OFFICIAL_DISTRIBUTION_PUBLICATION_VERSION,
official_release_id: official_release_id.to_string(),
mapping_identity,
manifest_identity: blake3::hash(&manifest_bytes).to_hex().to_string(),
entry_count: manifest.entries.len() as u64,
};
let anchor_path = release_root.join(OFFICIAL_DISTRIBUTION_PUBLICATION_FILE);
ensure_safe_file_target(release_root, &anchor_path, "官方 distribution publication")?;
let anchor_bytes = serde_json::to_vec_pretty(&anchor)
.map_err(|error| format!("序列化官方 distribution publication 失败:{error}"))?;
write_file_atomic(
&anchor_path,
&anchor_bytes,
STATE_FILE_MODE,
"官方 distribution publication",
)
}
/// Verifies a published official distribution anchor without hashing resource
/// files or canonicalizing the complete mapping.
pub(crate) fn verify_official_distribution_publication_at(
release_root: &Path,
expected_release_id: &str,
) -> Result<Option<OfficialDistributionPublicationAnchor>, String> {
ensure_safe_directory_path(release_root, "官方 distribution publication 根目录")?;
let anchor_path = release_root.join(OFFICIAL_DISTRIBUTION_PUBLICATION_FILE);
let Some(anchor_bytes) = read_file_no_symlink(&anchor_path, "官方 distribution publication")?
else {
return Ok(None);
};
let anchor: OfficialDistributionPublicationAnchor = serde_json::from_slice(&anchor_bytes)
.map_err(|error| format!("解析官方 distribution publication 失败:{error}"))?;
if anchor.version != OFFICIAL_DISTRIBUTION_PUBLICATION_VERSION
|| anchor.official_release_id != expected_release_id
|| anchor.mapping_identity.is_empty()
|| anchor.manifest_identity.is_empty()
{
return Err(format!(
"官方 distribution publication anchor 不一致:{}",
anchor_path.display()
));
}
let manifest_path = release_root.join(DOWNLOAD_MANIFEST_FILE);
let Some(manifest_bytes) = read_file_no_symlink(&manifest_path, "官方下载 manifest")?
else {
return Err(format!(
"官方 distribution publication 缺少 manifest{}",
manifest_path.display()
));
};
let manifest: OfficialDownloadManifest =
serde_json::from_slice(&manifest_bytes).map_err(|error| {
format!(
"解析官方下载 manifest 失败 {}{error}",
manifest_path.display()
)
})?;
if manifest.version != DOWNLOAD_MANIFEST_VERSION {
return Err(format!(
"不支持的官方下载 manifest 版本:{}",
manifest.version
));
}
if manifest.distribution_mapping_identity.as_deref() != Some(anchor.mapping_identity.as_str())
|| blake3::hash(&manifest_bytes).to_hex().to_string() != anchor.manifest_identity
|| manifest.entries.len() as u64 != anchor.entry_count
|| manifest.destination_index.len() as u64 != anchor.entry_count
{
return Err(format!(
"官方 distribution publication 与 manifest 不一致:{}",
release_root.display()
));
}
Ok(Some(anchor))
}
fn legacy_source_mapping_identity(release_root: &Path) -> Result<String, String> {
let Some(manifest) = read_download_manifest_at(release_root)? else {
return Ok("missing-official-manifest".to_string());
@@ -602,11 +756,11 @@ fn resolve_legacy_ownership(
return Ok((release_id, true));
}
if cas_has_ownership(cas_root, &release_id)? {
scope
.legacy_basename_compatibility
.insert(release_id.clone());
write_owner_scope_state(release_root, &scope)?;
return Ok((release_id, true));
return Err(format!(
"ambiguous_legacy_ownership: CAS basename ledger {} 无法证明属于 output root {};请先显式记录 compatibility marker",
release_id,
ownership_scope_root(release_root).display()
));
}
let source_identity = legacy_source_mapping_identity(release_root)?;
@@ -5088,6 +5242,11 @@ exit 22
let object_id = store_cas_object(&cas_root, b"legacy object");
let release_root = temp.path().join("legacy-release");
fs::create_dir_all(&release_root).unwrap();
let mut scope = load_owner_scope_state(&release_root).unwrap();
scope
.legacy_basename_compatibility
.insert("legacy-release".to_string());
write_owner_scope_state(&release_root, &scope).unwrap();
fs::write(
release_root.join(OFFICIAL_CAS_REUSE_REFERENCES_FILE),
serde_json::to_vec(&OfficialCasReuseReferenceManifest {
@@ -5112,6 +5271,61 @@ exit 22
assert_eq!(cas_reference_count(&cas_root, &object_id), 1);
}
#[test]
fn legacy_basename_ledger_without_root_marker_is_ambiguous() {
let temp = TempDir::new().unwrap();
let cas_root = temp.path().join("cas");
let output_a = temp.path().join("output-a/versions/release-x");
let output_b = temp.path().join("output-b/versions/release-x");
fs::create_dir_all(&output_a).unwrap();
fs::create_dir_all(&output_b).unwrap();
let object_id = store_cas_object(&cas_root, b"shared legacy object");
let cas = crate::FileSystemCasRepository::new(&cas_root);
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
cas.add_reference(&object_id).await.unwrap();
cas.add_reference(&object_id).await.unwrap();
assert!(cas
.release_reference_once("release-x", 0, &object_id)
.await
.unwrap());
});
let mut output_a_scope = load_owner_scope_state(&output_a).unwrap();
output_a_scope
.legacy_basename_compatibility
.insert("release-x".to_string());
write_owner_scope_state(&output_a, &output_a_scope).unwrap();
let manifest_path = output_b.join(OFFICIAL_CAS_REUSE_REFERENCES_FILE);
let manifest_bytes = serde_json::to_vec(&OfficialCasReuseReferenceManifest {
version: OFFICIAL_CAS_REUSE_REFERENCES_VERSION,
ownership_id: None,
object_ids: vec![object_id.clone()],
})
.unwrap();
fs::write(&manifest_path, &manifest_bytes).unwrap();
let result = release_cas_reuse_references(&output_b, &cas_root);
let error = result.unwrap_err();
assert!(error.contains("ambiguous_legacy_ownership"));
assert_eq!(cas_reference_count(&cas_root, &object_id), 2);
let ledger_exists = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async { cas.has_release_ownership("release-x").await.unwrap() });
assert!(ledger_exists);
assert_eq!(fs::read(&manifest_path).unwrap(), manifest_bytes);
assert!(output_b.is_dir());
let output_b_scope = load_owner_scope_state(&output_b).unwrap();
assert!(!output_b_scope
.legacy_basename_compatibility
.contains("release-x"));
}
#[test]
fn legacy_manifest_migration_persists_identity_before_retry() {
let temp = TempDir::new().unwrap();
@@ -5161,6 +5375,11 @@ exit 22
let cas_root = temp.path().join("cas");
let first = temp.path().join("versions").join("release-x");
fs::create_dir_all(&first).unwrap();
let mut scope = load_owner_scope_state(&first).unwrap();
scope
.legacy_basename_compatibility
.insert("release-x".to_string());
write_owner_scope_state(&first, &scope).unwrap();
let first_object = store_cas_object(&cas_root, b"legacy-first");
let second_object = store_cas_object(&cas_root, b"legacy-second");
let cas = crate::FileSystemCasRepository::new(&cas_root);