mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
fix: 抽象官方库存与 sidecar hash 策略
This commit is contained in:
@@ -23,8 +23,74 @@ pub struct PlatformCatalogInput<'a> {
|
|||||||
pub media_catalog: &'a [u8],
|
pub media_catalog: &'a [u8],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Platform catalog parser selected by an official resource backend.
|
||||||
|
pub trait InventoryParser: Send + Sync {
|
||||||
|
/// Parses verified seed catalog payloads into a platform-aware inventory.
|
||||||
|
fn parse_inventory(
|
||||||
|
&self,
|
||||||
|
table_catalog: &[u8],
|
||||||
|
platform_catalogs: &[PlatformCatalogInput<'_>],
|
||||||
|
) -> YostarJpPlatformDownloadInventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verification result returned by a sidecar hash strategy.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SidecarHashVerification {
|
||||||
|
/// Decimal or textual expected digest parsed from the sidecar.
|
||||||
|
pub expected: String,
|
||||||
|
/// Digest computed from the resource bytes.
|
||||||
|
pub actual: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hash sidecar policy independent from download orchestration.
|
||||||
|
pub trait SidecarHashStrategy: Send + Sync {
|
||||||
|
/// Stable algorithm identifier used in diagnostics.
|
||||||
|
fn algorithm_id(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Parses and verifies one resource payload against a sidecar.
|
||||||
|
fn verify(&self, data: &[u8], sidecar: &[u8]) -> Result<SidecarHashVerification, String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Official JP decimal `xxHash32(seed=0)` sidecar strategy.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct XxHash32DecimalSeedZero;
|
||||||
|
|
||||||
|
impl XxHash32DecimalSeedZero {
|
||||||
|
/// Computes the decimal digest used by this sidecar strategy.
|
||||||
|
pub fn digest(self, data: &[u8]) -> String {
|
||||||
|
xxhash32(data).to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SidecarHashStrategy for XxHash32DecimalSeedZero {
|
||||||
|
fn algorithm_id(&self) -> &'static str {
|
||||||
|
"xxhash32_decimal"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(&self, data: &[u8], sidecar: &[u8]) -> Result<SidecarHashVerification, String> {
|
||||||
|
let expected = std::str::from_utf8(sidecar)
|
||||||
|
.map_err(|error| format!("官方 hash sidecar 不是 UTF-8:{error}"))?
|
||||||
|
.trim()
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|error| format!("官方 hash sidecar 不是十进制 xxHash32:{error}"))?;
|
||||||
|
let actual = self.digest(data);
|
||||||
|
let mismatch = expected.to_string() != actual;
|
||||||
|
let verification = SidecarHashVerification {
|
||||||
|
expected: expected.to_string(),
|
||||||
|
actual,
|
||||||
|
};
|
||||||
|
if mismatch {
|
||||||
|
return Err(format!(
|
||||||
|
"官方 hash 校验失败:期望 {},实际 {}",
|
||||||
|
verification.expected, verification.actual
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(verification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Region/backend contract used by official resource orchestration.
|
/// Region/backend contract used by official resource orchestration.
|
||||||
pub trait OfficialResourceBackend: Send + Sync {
|
pub trait OfficialResourceBackend: InventoryParser + Send + Sync {
|
||||||
/// Stable backend identifier persisted in diagnostics.
|
/// Stable backend identifier persisted in diagnostics.
|
||||||
fn backend_id(&self) -> &'static str;
|
fn backend_id(&self) -> &'static str;
|
||||||
|
|
||||||
@@ -40,13 +106,6 @@ pub trait OfficialResourceBackend: Send + Sync {
|
|||||||
platforms: &[PatchPlatform],
|
platforms: &[PatchPlatform],
|
||||||
) -> Result<YostarJpResourceDiscoveryPlan, String>;
|
) -> Result<YostarJpResourceDiscoveryPlan, String>;
|
||||||
|
|
||||||
/// Builds platform-aware inventory from the verified seed catalog bytes.
|
|
||||||
fn inventory(
|
|
||||||
&self,
|
|
||||||
table_catalog: &[u8],
|
|
||||||
platform_catalogs: &[PlatformCatalogInput<'_>],
|
|
||||||
) -> YostarJpPlatformDownloadInventory;
|
|
||||||
|
|
||||||
/// Validates that a URL belongs to this backend's official hosts.
|
/// Validates that a URL belongs to this backend's official hosts.
|
||||||
fn is_official_url(&self, url: &str) -> bool;
|
fn is_official_url(&self, url: &str) -> bool;
|
||||||
}
|
}
|
||||||
@@ -61,6 +120,26 @@ pub trait DownloadUrlMapper: Send + Sync {
|
|||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
pub struct YostarJpBackend;
|
pub struct YostarJpBackend;
|
||||||
|
|
||||||
|
impl InventoryParser for YostarJpBackend {
|
||||||
|
fn parse_inventory(
|
||||||
|
&self,
|
||||||
|
table_catalog: &[u8],
|
||||||
|
platform_catalogs: &[PlatformCatalogInput<'_>],
|
||||||
|
) -> YostarJpPlatformDownloadInventory {
|
||||||
|
let catalogs = platform_catalogs
|
||||||
|
.iter()
|
||||||
|
.map(|catalog| {
|
||||||
|
YostarJpPlatformCatalogInventory::from_catalog_bytes(
|
||||||
|
catalog.platform,
|
||||||
|
catalog.bundle_packing_info,
|
||||||
|
catalog.media_catalog,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
YostarJpPlatformDownloadInventory::from_catalog_bytes(table_catalog, catalogs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl OfficialResourceBackend for YostarJpBackend {
|
impl OfficialResourceBackend for YostarJpBackend {
|
||||||
fn backend_id(&self) -> &'static str {
|
fn backend_id(&self) -> &'static str {
|
||||||
"bluearchive.yostar.jp"
|
"bluearchive.yostar.jp"
|
||||||
@@ -80,29 +159,77 @@ impl OfficialResourceBackend for YostarJpBackend {
|
|||||||
server_info.discovery_plan(connection_group, app_version, platforms)
|
server_info.discovery_plan(connection_group, app_version, platforms)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inventory(
|
|
||||||
&self,
|
|
||||||
table_catalog: &[u8],
|
|
||||||
platform_catalogs: &[PlatformCatalogInput<'_>],
|
|
||||||
) -> YostarJpPlatformDownloadInventory {
|
|
||||||
let catalogs = platform_catalogs
|
|
||||||
.iter()
|
|
||||||
.map(|catalog| {
|
|
||||||
YostarJpPlatformCatalogInventory::from_catalog_bytes(
|
|
||||||
catalog.platform,
|
|
||||||
catalog.bundle_packing_info,
|
|
||||||
catalog.media_catalog,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
YostarJpPlatformDownloadInventory::from_catalog_bytes(table_catalog, catalogs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_official_url(&self, url: &str) -> bool {
|
fn is_official_url(&self, url: &str) -> bool {
|
||||||
is_official_yostar_jp_url(url)
|
is_official_yostar_jp_url(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn xxhash32(bytes: &[u8]) -> u32 {
|
||||||
|
const PRIME1: u32 = 0x9E37_79B1;
|
||||||
|
const PRIME2: u32 = 0x85EB_CA77;
|
||||||
|
const PRIME3: u32 = 0xC2B2_AE3D;
|
||||||
|
const PRIME4: u32 = 0x27D4_EB2F;
|
||||||
|
const PRIME5: u32 = 0x1656_67B1;
|
||||||
|
|
||||||
|
let len = bytes.len();
|
||||||
|
let mut index = 0usize;
|
||||||
|
let mut hash = if len >= 16 {
|
||||||
|
let mut v1 = PRIME1.wrapping_add(PRIME2);
|
||||||
|
let mut v2 = PRIME2;
|
||||||
|
let mut v3 = 0;
|
||||||
|
let mut v4 = 0u32.wrapping_sub(PRIME1);
|
||||||
|
while index + 16 <= len {
|
||||||
|
v1 = xxhash32_round(v1, read_u32_le(bytes, index));
|
||||||
|
v2 = xxhash32_round(v2, read_u32_le(bytes, index + 4));
|
||||||
|
v3 = xxhash32_round(v3, read_u32_le(bytes, index + 8));
|
||||||
|
v4 = xxhash32_round(v4, read_u32_le(bytes, index + 12));
|
||||||
|
index += 16;
|
||||||
|
}
|
||||||
|
v1.rotate_left(1)
|
||||||
|
.wrapping_add(v2.rotate_left(7))
|
||||||
|
.wrapping_add(v3.rotate_left(12))
|
||||||
|
.wrapping_add(v4.rotate_left(18))
|
||||||
|
} else {
|
||||||
|
PRIME5
|
||||||
|
}
|
||||||
|
.wrapping_add(len as u32);
|
||||||
|
|
||||||
|
while index + 4 <= len {
|
||||||
|
hash = hash
|
||||||
|
.wrapping_add(read_u32_le(bytes, index).wrapping_mul(PRIME3))
|
||||||
|
.rotate_left(17)
|
||||||
|
.wrapping_mul(PRIME4);
|
||||||
|
index += 4;
|
||||||
|
}
|
||||||
|
while index < len {
|
||||||
|
hash = hash
|
||||||
|
.wrapping_add((bytes[index] as u32).wrapping_mul(PRIME5))
|
||||||
|
.rotate_left(11)
|
||||||
|
.wrapping_mul(PRIME1);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
hash ^= hash >> 15;
|
||||||
|
hash = hash.wrapping_mul(PRIME2);
|
||||||
|
hash ^= hash >> 13;
|
||||||
|
hash = hash.wrapping_mul(PRIME3);
|
||||||
|
hash ^ (hash >> 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xxhash32_round(acc: u32, input: u32) -> u32 {
|
||||||
|
acc.wrapping_add(input.wrapping_mul(0x85EB_CA77))
|
||||||
|
.rotate_left(13)
|
||||||
|
.wrapping_mul(0x9E37_79B1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
|
||||||
|
u32::from_le_bytes([
|
||||||
|
bytes[offset],
|
||||||
|
bytes[offset + 1],
|
||||||
|
bytes[offset + 2],
|
||||||
|
bytes[offset + 3],
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
impl DownloadUrlMapper for YostarJpBackend {
|
impl DownloadUrlMapper for YostarJpBackend {
|
||||||
fn relative_destination(&self, url: &str) -> Result<PathBuf, String> {
|
fn relative_destination(&self, url: &str) -> Result<PathBuf, String> {
|
||||||
let rest = url
|
let rest = url
|
||||||
@@ -182,7 +309,7 @@ mod tests {
|
|||||||
assert_eq!(backend.backend_id(), "bluearchive.yostar.jp");
|
assert_eq!(backend.backend_id(), "bluearchive.yostar.jp");
|
||||||
assert!(backend.is_official_url(&plan.endpoints[0].url));
|
assert!(backend.is_official_url(&plan.endpoints[0].url));
|
||||||
|
|
||||||
let inventory = backend.inventory(
|
let inventory = backend.parse_inventory(
|
||||||
b"ExcelDB.db ExcelDB.db",
|
b"ExcelDB.db ExcelDB.db",
|
||||||
&[PlatformCatalogInput {
|
&[PlatformCatalogInput {
|
||||||
platform: PatchPlatform::Windows,
|
platform: PatchPlatform::Windows,
|
||||||
@@ -194,6 +321,20 @@ mod tests {
|
|||||||
assert_eq!(inventory.platform_catalogs.len(), 1);
|
assert_eq!(inventory.platform_catalogs.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jp_hash_strategy_verifies_decimal_xxhash32_sidecars() {
|
||||||
|
let strategy = XxHash32DecimalSeedZero;
|
||||||
|
assert_eq!(strategy.algorithm_id(), "xxhash32_decimal");
|
||||||
|
assert_eq!(
|
||||||
|
strategy.verify(b"", b"46947589").unwrap(),
|
||||||
|
SidecarHashVerification {
|
||||||
|
expected: "46947589".to_string(),
|
||||||
|
actual: "46947589".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(strategy.verify(b"changed", b"46947589").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn jp_backend_maps_and_rejects_unsafe_destinations() {
|
fn jp_backend_maps_and_rejects_unsafe_destinations() {
|
||||||
let backend = YostarJpBackend;
|
let backend = YostarJpBackend;
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ pub mod launcher;
|
|||||||
pub mod yostar_jp;
|
pub mod yostar_jp;
|
||||||
|
|
||||||
pub use backend::{
|
pub use backend::{
|
||||||
destination_under_root, DownloadUrlMapper, OfficialResourceBackend, PlatformCatalogInput,
|
destination_under_root, DownloadUrlMapper, InventoryParser, OfficialResourceBackend,
|
||||||
|
PlatformCatalogInput, SidecarHashStrategy, SidecarHashVerification, XxHash32DecimalSeedZero,
|
||||||
YostarJpBackend,
|
YostarJpBackend,
|
||||||
};
|
};
|
||||||
pub use game_main_config::YostarJpGameMainConfig;
|
pub use game_main_config::YostarJpGameMainConfig;
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ use crate::zip_validation::{
|
|||||||
};
|
};
|
||||||
use bat_adapters::official::yostar_jp::YostarJpResourceEndpointKind;
|
use bat_adapters::official::yostar_jp::YostarJpResourceEndpointKind;
|
||||||
use bat_adapters::official::{
|
use bat_adapters::official::{
|
||||||
destination_under_root, DownloadUrlMapper, OfficialResourceBackend, YostarJpBackend,
|
destination_under_root, DownloadUrlMapper, OfficialResourceBackend, SidecarHashStrategy,
|
||||||
|
XxHash32DecimalSeedZero, YostarJpBackend,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
@@ -1835,12 +1836,15 @@ pub fn verify_official_seed_catalog_hash(
|
|||||||
hash_url: &str,
|
hash_url: &str,
|
||||||
hash_bytes: &[u8],
|
hash_bytes: &[u8],
|
||||||
) -> Result<OfficialResourceHashVerification, String> {
|
) -> Result<OfficialResourceHashVerification, String> {
|
||||||
let expected = parse_official_xxhash32_decimal(hash_url, hash_bytes)?;
|
let strategy = XxHash32DecimalSeedZero;
|
||||||
let actual = xxhash32(data);
|
let verification = strategy
|
||||||
|
.verify(data, hash_bytes)
|
||||||
|
.map_err(|error| format!("{error}:{hash_url}"))?;
|
||||||
|
|
||||||
if actual != expected {
|
if verification.expected != verification.actual {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"官方 hash 校验失败 {data_url}:期望 {expected}(来自 {hash_url}),实际 {actual}"
|
"官方 hash 校验失败 {data_url}:期望 {}(来自 {hash_url}),实际 {}",
|
||||||
|
verification.expected, verification.actual
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1848,8 +1852,8 @@ pub fn verify_official_seed_catalog_hash(
|
|||||||
data_url: data_url.to_string(),
|
data_url: data_url.to_string(),
|
||||||
hash_url: hash_url.to_string(),
|
hash_url: hash_url.to_string(),
|
||||||
algorithm: OfficialResourceHashAlgorithm::XxHash32Decimal,
|
algorithm: OfficialResourceHashAlgorithm::XxHash32Decimal,
|
||||||
expected: expected.to_string(),
|
expected: verification.expected,
|
||||||
actual: actual.to_string(),
|
actual: verification.actual,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2209,92 +2213,6 @@ fn blake3_file_hex(path: &Path) -> Result<String, String> {
|
|||||||
Ok(hasher.finalize().to_hex().to_string())
|
Ok(hasher.finalize().to_hex().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_official_xxhash32_decimal(hash_url: &str, bytes: &[u8]) -> Result<u32, String> {
|
|
||||||
let text = std::str::from_utf8(bytes)
|
|
||||||
.map_err(|error| format!("官方 hash sidecar 不是 UTF-8:{hash_url}:{error}"))?
|
|
||||||
.trim();
|
|
||||||
text.parse::<u32>()
|
|
||||||
.map_err(|error| format!("官方 hash sidecar 不是十进制 xxHash32:{hash_url}:{error}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xxhash32(bytes: &[u8]) -> u32 {
|
|
||||||
const PRIME1: u32 = 0x9E37_79B1;
|
|
||||||
const PRIME2: u32 = 0x85EB_CA77;
|
|
||||||
const PRIME3: u32 = 0xC2B2_AE3D;
|
|
||||||
const PRIME4: u32 = 0x27D4_EB2F;
|
|
||||||
const PRIME5: u32 = 0x1656_67B1;
|
|
||||||
|
|
||||||
let len = bytes.len();
|
|
||||||
let mut index = 0usize;
|
|
||||||
let mut hash = if len >= 16 {
|
|
||||||
let mut v1 = PRIME1.wrapping_add(PRIME2);
|
|
||||||
let mut v2 = PRIME2;
|
|
||||||
let mut v3 = 0;
|
|
||||||
let mut v4 = 0u32.wrapping_sub(PRIME1);
|
|
||||||
|
|
||||||
while index + 16 <= len {
|
|
||||||
v1 = xxhash32_round(v1, read_u32_le(bytes, index));
|
|
||||||
v2 = xxhash32_round(v2, read_u32_le(bytes, index + 4));
|
|
||||||
v3 = xxhash32_round(v3, read_u32_le(bytes, index + 8));
|
|
||||||
v4 = xxhash32_round(v4, read_u32_le(bytes, index + 12));
|
|
||||||
index += 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
v1.rotate_left(1)
|
|
||||||
.wrapping_add(v2.rotate_left(7))
|
|
||||||
.wrapping_add(v3.rotate_left(12))
|
|
||||||
.wrapping_add(v4.rotate_left(18))
|
|
||||||
} else {
|
|
||||||
PRIME5
|
|
||||||
}
|
|
||||||
.wrapping_add(len as u32);
|
|
||||||
|
|
||||||
while index + 4 <= len {
|
|
||||||
hash = hash
|
|
||||||
.wrapping_add(read_u32_le(bytes, index).wrapping_mul(PRIME3))
|
|
||||||
.rotate_left(17)
|
|
||||||
.wrapping_mul(PRIME4);
|
|
||||||
index += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
while index < len {
|
|
||||||
hash = hash
|
|
||||||
.wrapping_add((bytes[index] as u32).wrapping_mul(PRIME5))
|
|
||||||
.rotate_left(11)
|
|
||||||
.wrapping_mul(PRIME1);
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
xxhash32_avalanche(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xxhash32_round(acc: u32, input: u32) -> u32 {
|
|
||||||
const PRIME2: u32 = 0x85EB_CA77;
|
|
||||||
const PRIME1: u32 = 0x9E37_79B1;
|
|
||||||
|
|
||||||
acc.wrapping_add(input.wrapping_mul(PRIME2))
|
|
||||||
.rotate_left(13)
|
|
||||||
.wrapping_mul(PRIME1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xxhash32_avalanche(mut hash: u32) -> u32 {
|
|
||||||
hash ^= hash >> 15;
|
|
||||||
hash = hash.wrapping_mul(0x85EB_CA77);
|
|
||||||
hash ^= hash >> 13;
|
|
||||||
hash = hash.wrapping_mul(0xC2B2_AE3D);
|
|
||||||
hash ^= hash >> 16;
|
|
||||||
hash
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
|
|
||||||
u32::from_le_bytes([
|
|
||||||
bytes[offset],
|
|
||||||
bytes[offset + 1],
|
|
||||||
bytes[offset + 2],
|
|
||||||
bytes[offset + 3],
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -2311,6 +2229,13 @@ mod tests {
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn xxhash32(bytes: &[u8]) -> u32 {
|
||||||
|
XxHash32DecimalSeedZero
|
||||||
|
.digest(bytes)
|
||||||
|
.parse()
|
||||||
|
.expect("xxHash32 decimal digest")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fetch_bytes_maps_rejections_to_error_codes() {
|
fn fetch_bytes_maps_rejections_to_error_codes() {
|
||||||
// 非官方 URL:安全边界拒绝。
|
// 非官方 URL:安全边界拒绝。
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ use bat_adapters::official::yostar_jp::{
|
|||||||
YostarJpResourceEndpointKind, YostarJpServerInfo, YostarJpSyncSnapshot,
|
YostarJpResourceEndpointKind, YostarJpServerInfo, YostarJpSyncSnapshot,
|
||||||
};
|
};
|
||||||
use bat_adapters::official::{
|
use bat_adapters::official::{
|
||||||
OfficialResourceBackend, PlatformCatalogInput, YostarJpBackend,
|
InventoryParser, OfficialResourceBackend, PlatformCatalogInput, YostarJpBackend,
|
||||||
YostarJpPlatformDownloadInventory,
|
YostarJpPlatformDownloadInventory,
|
||||||
};
|
};
|
||||||
use bat_core::ErrorCode;
|
use bat_core::ErrorCode;
|
||||||
@@ -4141,7 +4141,7 @@ fn build_inventory_from_seed_catalogs(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(YostarJpBackend.inventory(table_catalog, &platform_catalogs))
|
Ok(YostarJpBackend.parse_inventory(table_catalog, &platform_catalogs))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
|||||||
Reference in New Issue
Block a user