fix(release): 完成分发身份与 CAS legacy ownership 收尾
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 10:44:24 +08:00
parent 786b739f99
commit 5bae90cb14
14 changed files with 867 additions and 109 deletions
+222 -36
View File
@@ -400,6 +400,15 @@ pub struct LocalizedDistributionManifest {
pub official_release_id: String,
/// Localized release identity.
pub localized_release_id: String,
/// Deterministic identity of the complete source official mapping.
#[serde(default)]
pub source_mapping_identity: String,
/// Deterministic identity of this localized mapping and its source.
#[serde(default)]
pub localized_mapping_identity: String,
/// Persisted destination-to-entry index for single-entry lookup.
#[serde(default)]
pub destination_index: BTreeMap<String, usize>,
/// Actual metadata for every official manifest entry.
pub entries: Vec<LocalizedDistributionEntry>,
}
@@ -841,7 +850,7 @@ impl LocalizedPatchService {
)
})?;
verify_localized_release_files(&version_path, &manifest)?;
verify_localized_distribution_manifest_at(&version_path, &current_release_id)?;
verify_localized_distribution_manifest_at(None, &version_path, &current_release_id)?;
if manifest.localized_release_id != current_release_id {
return Err(anyhow::anyhow!(
"manifest release={} 与当前状态 release={} 不一致",
@@ -900,6 +909,7 @@ impl LocalizedPatchService {
);
verify_localized_release_files(&previous_path, restored_manifest.as_ref().unwrap())?;
verify_localized_distribution_manifest_at(
None,
&previous_path,
restored_release_id.as_deref().unwrap_or_default(),
)?;
@@ -2178,7 +2188,11 @@ fn verify_published_localized_release(
&manifest,
unzip_command,
)?;
verify_localized_distribution_manifest_at(version_path, &manifest.localized_release_id)?;
verify_localized_distribution_manifest_at(
Some(official_release_root),
version_path,
&manifest.localized_release_id,
)?;
integrity.current_points_to_release = current_points_to_version(current_path, version_path)?;
if !integrity.current_points_to_release {
return Err(anyhow::anyhow!(
@@ -2218,6 +2232,8 @@ fn build_localized_distribution_manifest(
else {
return Ok(None);
};
let source_mapping_identity =
crate::official_download::official_distribution_mapping_identity(&official_manifest);
let mut entries = Vec::with_capacity(official_manifest.entries.len());
for entry in official_manifest.entries.values() {
let path = staging_root.join(&entry.destination);
@@ -2232,10 +2248,25 @@ fn build_localized_distribution_manifest(
blake3: blake3::hash(&bytes).to_hex().to_string(),
});
}
let destination_index = entries
.iter()
.enumerate()
.map(|(index, entry)| (entry.destination.clone(), index))
.collect::<BTreeMap<_, _>>();
if destination_index.len() != entries.len() {
return Err(anyhow::anyhow!(
"official distribution manifest 存在重复 destination"
));
}
let localized_mapping_identity =
localized_distribution_mapping_identity(&source_mapping_identity, &entries);
Ok(Some(LocalizedDistributionManifest {
version: 1,
official_release_id: config.release_id.clone(),
localized_release_id: config.published_release_id().to_string(),
source_mapping_identity,
localized_mapping_identity,
destination_index,
entries,
}))
}
@@ -2267,6 +2298,7 @@ fn verify_localized_release_files(
}
fn verify_localized_distribution_manifest_at(
official_release_root: Option<&Path>,
version_path: &Path,
localized_release_id: &str,
) -> anyhow::Result<()> {
@@ -2286,6 +2318,65 @@ fn verify_localized_distribution_manifest_at(
manifest.localized_release_id
));
}
let source_mapping_identity = if let Some(official_release_root) = official_release_root {
let official_manifest =
crate::official_download::read_download_manifest_at(official_release_root)
.map_err(anyhow::Error::msg)?
.ok_or_else(|| {
anyhow::anyhow!("localized distribution 缺少 source official manifest")
})?;
let source_mapping_identity =
crate::official_download::official_distribution_mapping_identity(&official_manifest);
let expected_destination_index =
crate::official_download::official_distribution_destination_index(&official_manifest)
.map_err(anyhow::Error::msg)?;
if manifest.source_mapping_identity != source_mapping_identity
|| official_manifest.distribution_mapping_identity.as_deref()
!= Some(source_mapping_identity.as_str())
|| official_manifest.destination_index != expected_destination_index
{
return Err(anyhow::anyhow!(
"localized distribution source mapping/index 不一致:expected={} actual={} official={:?}",
source_mapping_identity,
manifest.source_mapping_identity,
official_manifest.distribution_mapping_identity
));
}
if !localized_distribution_entries_match_official(&manifest, &official_manifest) {
return Err(anyhow::anyhow!(
"localized distribution manifest 与 source official mapping 不一致"
));
}
source_mapping_identity
} else {
manifest.source_mapping_identity.clone()
};
if !manifest.source_mapping_identity.is_empty() {
let localized_mapping_identity =
localized_distribution_mapping_identity(&source_mapping_identity, &manifest.entries);
if manifest.localized_mapping_identity != localized_mapping_identity {
return Err(anyhow::anyhow!(
"localized distribution mapping identity 不一致:expected={} actual={}",
localized_mapping_identity,
manifest.localized_mapping_identity
));
}
if manifest.destination_index.len() != manifest.entries.len()
|| manifest
.destination_index
.iter()
.any(|(destination, index)| {
manifest
.entries
.get(*index)
.is_none_or(|entry| entry.destination != *destination)
})
{
return Err(anyhow::anyhow!(
"localized distribution destination index 不一致"
));
}
}
let mut destinations = BTreeSet::new();
for entry in &manifest.entries {
if !destinations.insert(entry.destination.as_str()) {
@@ -2314,6 +2405,69 @@ fn verify_localized_distribution_manifest_at(
Ok(())
}
pub(crate) fn verify_localized_distribution_manifest_for_status(
official_release_root: &Path,
version_path: &Path,
localized_release_id: &str,
) -> anyhow::Result<()> {
verify_localized_distribution_manifest_at(
Some(official_release_root),
version_path,
localized_release_id,
)
}
fn localized_distribution_entries_match_official(
localized: &LocalizedDistributionManifest,
official: &crate::official_download::OfficialDownloadManifest,
) -> bool {
if localized.entries.len() != official.entries.len() {
return false;
}
let mut localized_by_destination = BTreeMap::new();
for entry in &localized.entries {
if localized_by_destination
.insert(entry.destination.as_str(), entry.url.as_str())
.is_some()
{
return false;
}
}
official.entries.values().all(|entry| {
localized_by_destination.get(entry.destination.as_str()) == Some(&entry.url.as_str())
})
}
fn localized_distribution_mapping_identity(
source_mapping_identity: &str,
entries: &[LocalizedDistributionEntry],
) -> String {
let mut ordered = entries.iter().collect::<Vec<_>>();
ordered.sort_by(|left, right| {
left.destination
.cmp(&right.destination)
.then_with(|| left.url.cmp(&right.url))
.then_with(|| left.bytes.cmp(&right.bytes))
.then_with(|| left.blake3.cmp(&right.blake3))
});
let mut hasher = blake3::Hasher::new();
hasher.update(b"localized-distribution-mapping-v1");
update_distribution_identity_string(&mut hasher, source_mapping_identity);
hasher.update(&(ordered.len() as u64).to_be_bytes());
for entry in ordered {
update_distribution_identity_string(&mut hasher, &entry.destination);
update_distribution_identity_string(&mut hasher, &entry.url);
hasher.update(&entry.bytes.to_be_bytes());
update_distribution_identity_string(&mut hasher, &entry.blake3);
}
format!("ldm-v1-{}", hasher.finalize().to_hex())
}
fn update_distribution_identity_string(hasher: &mut blake3::Hasher, value: &str) {
hasher.update(&(value.len() as u64).to_be_bytes());
hasher.update(value.as_bytes());
}
fn write_localized_transaction(
localized_output_root: &Path,
transaction: &LocalizedReleaseTransaction,
@@ -3996,42 +4150,50 @@ mod tests {
fs::write(target.join("data.json"), json_target).unwrap();
fs::write(official.join("text.txt"), text_source).unwrap();
fs::write(target.join("text.txt"), text_target).unwrap();
let mut official_manifest = crate::OfficialDownloadManifest {
version: 1,
entries: [
(
"https://example.invalid/data.bin".to_string(),
"data.bin",
binary_source.as_slice(),
),
(
"https://example.invalid/data.json".to_string(),
"data.json",
json_source.as_slice(),
),
(
"https://example.invalid/text.txt".to_string(),
"text.txt",
text_source.as_bytes(),
),
]
.into_iter()
.map(|(url, destination, bytes)| {
(
url.clone(),
crate::OfficialDownloadManifestEntry {
url,
destination: destination.to_string(),
bytes: bytes.len() as u64,
blake3: blake3::hash(bytes).to_hex().to_string(),
},
)
})
.collect(),
destination_index: BTreeMap::new(),
distribution_mapping_identity: None,
};
official_manifest.distribution_mapping_identity = Some(
crate::official_distribution_mapping_identity(&official_manifest),
);
official_manifest.destination_index =
crate::official_download::official_distribution_destination_index(&official_manifest)
.unwrap();
fs::write(
official.join("official-download-manifest.json"),
serde_json::to_vec(&crate::OfficialDownloadManifest {
version: 1,
entries: [
(
"https://example.invalid/data.bin".to_string(),
"data.bin",
binary_source.as_slice(),
),
(
"https://example.invalid/data.json".to_string(),
"data.json",
json_source.as_slice(),
),
(
"https://example.invalid/text.txt".to_string(),
"text.txt",
text_source.as_bytes(),
),
]
.into_iter()
.map(|(url, destination, bytes)| {
(
url.clone(),
crate::OfficialDownloadManifestEntry {
url,
destination: destination.to_string(),
bytes: bytes.len() as u64,
blake3: blake3::hash(bytes).to_hex().to_string(),
},
)
})
.collect(),
})
.unwrap(),
serde_json::to_vec(&official_manifest).unwrap(),
)
.unwrap();
@@ -4142,6 +4304,12 @@ mod tests {
)
.unwrap();
assert_eq!(distribution.entries.len(), 3);
assert_eq!(
distribution.source_mapping_identity,
crate::official_distribution_mapping_identity(&official_manifest)
);
assert_eq!(distribution.destination_index.len(), 3);
assert!(!distribution.localized_mapping_identity.is_empty());
for (path, expected) in [
("data.bin", binary_target.as_slice()),
("data.json", json_target.as_slice()),
@@ -4155,6 +4323,24 @@ mod tests {
assert_eq!(entry.bytes, expected.len() as u64);
assert_eq!(entry.blake3, blake3::hash(expected).to_hex().to_string());
}
let mut tampered_official = official_manifest;
tampered_official
.entries
.get_mut("https://example.invalid/data.json")
.unwrap()
.bytes += 1;
fs::write(
official.join("official-download-manifest.json"),
serde_json::to_vec(&tampered_official).unwrap(),
)
.unwrap();
assert!(verify_localized_distribution_manifest_for_status(
&official,
&report.version_path,
"localized-v1"
)
.is_err());
}
#[cfg(unix)]