mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-19 12:06:40 +08:00
fix(release): 完成分发身份与 CAS legacy ownership 收尾
This commit is contained in:
@@ -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, ¤t_release_id)?;
|
||||
verify_localized_distribution_manifest_at(None, &version_path, ¤t_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)]
|
||||
|
||||
Reference in New Issue
Block a user