//! Official JP resource pull planning. use bat_adapters::official::inventory::{ YostarJpDownloadInventory, YostarJpPlatformDownloadInventory, }; use bat_adapters::official::yostar_jp::{ verified_official_platforms, PatchPlatform, YostarJpResourceDiscoveryPlan, YostarJpResourceRoot, }; use std::collections::HashSet; /// Full official JP pull plan, covering discovery endpoints and concrete /// resource downloads. #[derive(Debug, Clone, PartialEq, Eq)] pub struct OfficialResourcePullPlan { /// Seed URLs and selected root from server-info. pub discovery: YostarJpResourceDiscoveryPlan, /// Concrete file inventory extracted from the catalog bytes. pub inventory: YostarJpPlatformDownloadInventory, /// Platforms that will be pulled. pub platforms: Vec, } impl OfficialResourcePullPlan { /// Creates a pull plan for the requested platforms. pub fn new( discovery: YostarJpResourceDiscoveryPlan, inventory: YostarJpDownloadInventory, platforms: &[PatchPlatform], ) -> Self { Self::new_platform_inventory( discovery, YostarJpPlatformDownloadInventory::from_shared_inventory(inventory, platforms), platforms, ) } /// Creates a pull plan for the requested platforms using platform-specific /// bundle/media catalogs. pub fn new_platform_inventory( discovery: YostarJpResourceDiscoveryPlan, inventory: YostarJpPlatformDownloadInventory, platforms: &[PatchPlatform], ) -> Self { Self { discovery, inventory, platforms: unique_platforms(platforms), } } /// Creates a pull plan for the verified official JP platforms. pub fn verified( discovery: YostarJpResourceDiscoveryPlan, inventory: YostarJpDownloadInventory, ) -> Self { Self::new(discovery, inventory, &verified_official_platforms()) } /// Creates a pull plan for the verified official JP platforms using /// platform-specific bundle/media catalogs. pub fn verified_platform_inventory( discovery: YostarJpResourceDiscoveryPlan, inventory: YostarJpPlatformDownloadInventory, ) -> Self { Self::new_platform_inventory(discovery, inventory, &verified_official_platforms()) } /// Returns all discovery URLs in server-provided order. pub fn discovery_urls(&self) -> Vec { self.discovery .endpoints .iter() .map(|endpoint| endpoint.url.clone()) .collect() } /// Returns the validated official resource root used by the pull plan. pub fn resource_root(&self) -> Result { YostarJpResourceRoot::from_addressables_root(&self.discovery.addressables_root) } /// Returns all concrete download URLs for the selected platforms. pub fn content_urls(&self) -> Result, String> { let root = self.resource_root()?; self.inventory .direct_download_urls_for_platforms(&root, &self.platforms) } /// Returns discovery URLs followed by concrete content URLs. pub fn all_urls(&self) -> Result, String> { let mut urls = self.discovery_urls(); let mut seen = urls.iter().cloned().collect::>(); for url in self.content_urls()? { if seen.insert(url.clone()) { urls.push(url); } } Ok(urls) } } /// Builds a pull plan for the verified official JP platforms. pub fn build_official_pull_plan( discovery: YostarJpResourceDiscoveryPlan, inventory: YostarJpDownloadInventory, ) -> OfficialResourcePullPlan { OfficialResourcePullPlan::verified(discovery, inventory) } /// Builds a pull plan for the verified official JP platforms using /// platform-specific bundle/media catalogs. pub fn build_official_pull_plan_from_platform_inventory( discovery: YostarJpResourceDiscoveryPlan, inventory: YostarJpPlatformDownloadInventory, ) -> OfficialResourcePullPlan { OfficialResourcePullPlan::verified_platform_inventory(discovery, inventory) } /// Builds a pull plan for the requested platforms. pub fn build_official_pull_plan_for_platforms( discovery: YostarJpResourceDiscoveryPlan, inventory: YostarJpDownloadInventory, platforms: &[PatchPlatform], ) -> OfficialResourcePullPlan { OfficialResourcePullPlan::new(discovery, inventory, platforms) } /// Builds a pull plan for the requested platforms using platform-specific /// bundle/media catalogs. pub fn build_official_pull_plan_for_platform_inventory( discovery: YostarJpResourceDiscoveryPlan, inventory: YostarJpPlatformDownloadInventory, platforms: &[PatchPlatform], ) -> OfficialResourcePullPlan { OfficialResourcePullPlan::new_platform_inventory(discovery, inventory, platforms) } fn unique_platforms(platforms: &[PatchPlatform]) -> Vec { platforms .iter() .copied() .fold(Vec::new(), |mut unique, platform| { if !unique.contains(&platform) { unique.push(platform); } unique }) } #[cfg(test)] mod tests { use super::*; use bat_adapters::official::inventory::{ YostarJpDownloadInventory, YostarJpPlatformCatalogInventory, YostarJpPlatformDownloadInventory, }; use bat_adapters::official::yostar_jp::{ YostarJpResourceEndpoint, YostarJpResourceEndpointKind, }; fn discovery_plan() -> YostarJpResourceDiscoveryPlan { YostarJpResourceDiscoveryPlan { connection_group_name: "Prod-Audit".to_string(), app_version: "1.70.0".to_string(), bundle_version: Some("s8tloc7lo3".to_string()), addressables_root: "https://prod-clientpatch.bluearchiveyostar.com/r93_token".to_string(), endpoints: vec![ YostarJpResourceEndpoint { kind: YostarJpResourceEndpointKind::TableCatalog, platform: None, url: "https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes".to_string(), }, YostarJpResourceEndpoint { kind: YostarJpResourceEndpointKind::TableCatalogHash, platform: None, url: "https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.hash".to_string(), }, ], } } fn inventory() -> YostarJpDownloadInventory { YostarJpDownloadInventory::from_catalog_bytes( b"FullPatch_000.zip", b"ExcelDB.db ExcelDB.db", b"JP_Airi.zip", ) } fn platform_inventory() -> YostarJpPlatformDownloadInventory { YostarJpPlatformDownloadInventory::from_catalog_bytes( b"ExcelDB.db ExcelDB.db", vec![ YostarJpPlatformCatalogInventory::from_catalog_bytes( PatchPlatform::Windows, b"FullPatch_000.zip", b"JP_Airi_Win.zip", ), YostarJpPlatformCatalogInventory::from_catalog_bytes( PatchPlatform::Android, b"FullPatch_001.zip", b"JP_Airi_Android.zip", ), ], ) } #[test] fn builds_verified_pull_plan_with_all_urls() { let plan = build_official_pull_plan(discovery_plan(), inventory()); assert_eq!(plan.platforms, verified_official_platforms()); assert_eq!(plan.discovery_urls().len(), 2); let content_urls = plan.content_urls().unwrap(); assert!(content_urls .iter() .any(|url| url.contains("/Windows_PatchPack/FullPatch_000.zip"))); assert!(content_urls .iter() .any(|url| url.contains("/Android_PatchPack/FullPatch_000.zip"))); assert_eq!(content_urls.len(), 5); let all_urls = plan.all_urls().unwrap(); assert_eq!(all_urls.len(), 7); assert!(all_urls[0].ends_with("TableCatalog.bytes")); assert!(all_urls .iter() .any(|url| url.ends_with("/MediaResources-Windows/JP_Airi.zip"))); assert_eq!( all_urls .iter() .filter(|url| url.ends_with("/MediaResources/JP_Airi.zip")) .count(), 1 ); } #[test] fn deduplicates_requested_platforms() { let plan = build_official_pull_plan_for_platforms( discovery_plan(), inventory(), &[PatchPlatform::Windows, PatchPlatform::Windows], ); assert_eq!(plan.platforms, vec![PatchPlatform::Windows]); } #[test] fn verified_pull_plan_is_windows_and_android_only() { let plan = build_official_pull_plan(discovery_plan(), inventory()); assert_eq!( plan.platforms, vec![PatchPlatform::Windows, PatchPlatform::Android] ); } #[test] fn builds_platform_inventory_pull_plan_without_cross_mixing_catalogs() { let plan = build_official_pull_plan_from_platform_inventory( discovery_plan(), platform_inventory(), ); let all_urls = plan.all_urls().unwrap(); assert_eq!(plan.platforms, verified_official_platforms()); assert_eq!(all_urls.len(), 7); assert!(all_urls .iter() .any(|url| url.ends_with("/Windows_PatchPack/FullPatch_000.zip"))); assert!(all_urls .iter() .any(|url| url.ends_with("/Android_PatchPack/FullPatch_001.zip"))); assert!(!all_urls .iter() .any(|url| url.ends_with("/Android_PatchPack/FullPatch_000.zip"))); } }