ngdp_client/
manifest.rs

1// Manifest abstraction layer for NGDP client
2// This module provides a unified interface for working with different manifest types
3// while maintaining explicit knowledge of key types throughout the pipeline
4
5use std::fmt;
6use tact_parser::download::DownloadManifest;
7use tact_parser::encoding::EncodingFile;
8use tact_parser::install::InstallManifest;
9
10/// Strongly typed content key (CKey)
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub struct ContentKey(pub Vec<u8>);
13
14impl ContentKey {
15    pub fn new(data: Vec<u8>) -> Self {
16        Self(data)
17    }
18
19    pub fn as_bytes(&self) -> &[u8] {
20        &self.0
21    }
22
23    pub fn to_hex(&self) -> String {
24        hex::encode(&self.0)
25    }
26}
27
28impl fmt::Display for ContentKey {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "CKey:{}", self.to_hex())
31    }
32}
33
34/// Strongly typed encoding key (EKey)
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub struct EncodingKey(pub Vec<u8>);
37
38impl EncodingKey {
39    pub fn new(data: Vec<u8>) -> Self {
40        Self(data)
41    }
42
43    pub fn as_bytes(&self) -> &[u8] {
44        &self.0
45    }
46
47    pub fn to_hex(&self) -> String {
48        hex::encode(&self.0)
49    }
50}
51
52impl fmt::Display for EncodingKey {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(f, "EKey:{}", self.to_hex())
55    }
56}
57
58/// A file entry from install manifest (has paths and CKeys)
59#[derive(Debug, Clone)]
60pub struct InstallManifestEntry {
61    /// Actual file path in the game installation
62    pub path: String,
63    /// Content key for this file
64    pub content_key: ContentKey,
65    /// File size in bytes
66    pub size: usize,
67}
68
69/// A file entry from download manifest (has EKeys but no paths)
70#[derive(Debug, Clone)]
71pub struct DownloadManifestEntry {
72    /// Generated path (since download manifest has no paths)
73    pub generated_path: String,
74    /// Encoding key for this file
75    pub encoding_key: EncodingKey,
76    /// Compressed size in bytes
77    pub compressed_size: usize,
78    /// Download priority (0 = highest)
79    pub priority: i32,
80}
81
82/// Type-safe manifest that knows its type at compile time
83#[derive(Debug)]
84pub enum TypedManifest {
85    Install(TypedInstallManifest),
86    Download(TypedDownloadManifest),
87}
88
89/// Strongly typed install manifest
90#[derive(Debug)]
91pub struct TypedInstallManifest {
92    entries: Vec<InstallManifestEntry>,
93}
94
95/// Strongly typed download manifest
96#[derive(Debug)]
97pub struct TypedDownloadManifest {
98    entries: Vec<DownloadManifestEntry>,
99}
100
101impl TypedInstallManifest {
102    pub fn from_raw(manifest: InstallManifest) -> Self {
103        let entries = manifest
104            .entries
105            .iter()
106            .map(|entry| InstallManifestEntry {
107                path: entry.path.clone(),
108                content_key: ContentKey::new(entry.ckey.clone()),
109                size: entry.size as usize,
110            })
111            .collect();
112
113        Self { entries }
114    }
115
116    pub fn entries(&self) -> &[InstallManifestEntry] {
117        &self.entries
118    }
119
120    /// Get entries that require CKey -> EKey resolution
121    pub fn entries_requiring_encoding_lookup(&self) -> Vec<&InstallManifestEntry> {
122        self.entries.iter().collect()
123    }
124}
125
126impl TypedDownloadManifest {
127    pub fn from_raw(manifest: DownloadManifest) -> Self {
128        let entries = manifest
129            .entries
130            .iter()
131            .enumerate()
132            .map(|(i, (ekey, entry))| DownloadManifestEntry {
133                generated_path: format!("data/{:08x}", i),
134                encoding_key: EncodingKey::new(ekey.clone()),
135                compressed_size: entry.compressed_size as usize,
136                priority: entry.priority as i32,
137            })
138            .collect();
139
140        Self { entries }
141    }
142
143    pub fn entries(&self) -> &[DownloadManifestEntry] {
144        &self.entries
145    }
146
147    /// Get entries that can be used directly with archive indices (already have EKeys)
148    pub fn entries_with_encoding_keys(&self) -> Vec<&DownloadManifestEntry> {
149        self.entries.iter().collect()
150    }
151}
152
153/// A downloadable file with resolved keys
154#[derive(Debug, Clone)]
155pub struct DownloadableFile {
156    /// Path where file will be saved
157    pub path: String,
158    /// Encoding key for downloading (what archives use)
159    pub encoding_key: EncodingKey,
160    /// Content key for verification (optional)
161    pub content_key: Option<ContentKey>,
162    /// Expected file size
163    pub size: usize,
164    /// Source manifest type (for debugging)
165    pub source: ManifestSource,
166}
167
168#[derive(Debug, Clone, Copy)]
169pub enum ManifestSource {
170    InstallManifest,
171    DownloadManifest,
172}
173
174/// Key resolver that maintains type safety
175pub struct TypedKeyResolver<'a> {
176    encoding_file: &'a EncodingFile,
177}
178
179impl<'a> TypedKeyResolver<'a> {
180    pub fn new(encoding_file: &'a EncodingFile) -> Self {
181        Self { encoding_file }
182    }
183
184    /// Convert install manifest entry to downloadable file (CKey -> EKey resolution)
185    pub fn resolve_install_entry(
186        &self,
187        entry: &InstallManifestEntry,
188    ) -> Result<DownloadableFile, String> {
189        // Look up EKey from CKey
190        let encoding_entry = self
191            .encoding_file
192            .lookup_by_ckey(entry.content_key.as_bytes())
193            .ok_or_else(|| format!("No encoding entry for {}", entry.content_key))?;
194
195        let encoding_key = encoding_entry
196            .encoding_keys
197            .first()
198            .ok_or_else(|| format!("No encoding keys for {}", entry.content_key))?;
199
200        Ok(DownloadableFile {
201            path: entry.path.clone(),
202            encoding_key: EncodingKey::new(encoding_key.clone()),
203            content_key: Some(entry.content_key.clone()),
204            size: self
205                .encoding_file
206                .get_file_size(entry.content_key.as_bytes())
207                .map(|s| s as usize)
208                .unwrap_or(entry.size),
209            source: ManifestSource::InstallManifest,
210        })
211    }
212
213    /// Convert download manifest entry to downloadable file (already has EKey)
214    pub fn resolve_download_entry(
215        &self,
216        entry: &DownloadManifestEntry,
217    ) -> Result<DownloadableFile, String> {
218        // Download manifest already has EKey, optionally look up CKey
219        let content_key = self
220            .encoding_file
221            .lookup_by_ekey(entry.encoding_key.as_bytes())
222            .map(|ckey| ContentKey::new(ckey.clone()));
223
224        let size = content_key
225            .as_ref()
226            .and_then(|ckey| self.encoding_file.get_file_size(ckey.as_bytes()))
227            .map(|s| s as usize)
228            .unwrap_or(entry.compressed_size);
229
230        Ok(DownloadableFile {
231            path: entry.generated_path.clone(),
232            encoding_key: entry.encoding_key.clone(),
233            content_key,
234            size,
235            source: ManifestSource::DownloadManifest,
236        })
237    }
238
239    /// Resolve any manifest to downloadable files
240    pub fn resolve_manifest(
241        &self,
242        manifest: &TypedManifest,
243    ) -> Vec<Result<DownloadableFile, String>> {
244        match manifest {
245            TypedManifest::Install(m) => m
246                .entries()
247                .iter()
248                .map(|e| self.resolve_install_entry(e))
249                .collect(),
250            TypedManifest::Download(m) => m
251                .entries()
252                .iter()
253                .map(|e| self.resolve_download_entry(e))
254                .collect(),
255        }
256    }
257}
258
259/// Installation strategy that knows which manifest to use
260#[derive(Debug, Clone, Copy)]
261pub enum InstallStrategy {
262    /// Use install manifest for accurate file paths and content verification
263    UseInstallManifest,
264    /// Use download manifest for streaming/partial downloads
265    UseDownloadManifest,
266    /// Automatically choose based on install type
267    Auto,
268}
269
270impl InstallStrategy {
271    /// Determine which manifest to use based on install type
272    pub fn select_manifest(self, install_type: InstallType) -> ManifestChoice {
273        match self {
274            Self::UseInstallManifest => ManifestChoice::Install,
275            Self::UseDownloadManifest => ManifestChoice::Download,
276            Self::Auto => {
277                match install_type {
278                    InstallType::Minimal => ManifestChoice::Download, // Better for partial
279                    InstallType::Full => ManifestChoice::Install,     // Better for complete
280                    InstallType::Custom(_) => ManifestChoice::Download, // Flexible priority
281                }
282            }
283        }
284    }
285}
286
287#[derive(Debug, Clone, Copy)]
288pub enum ManifestChoice {
289    Install,
290    Download,
291}
292
293/// Installation type
294#[derive(Debug, Clone, Copy)]
295pub enum InstallType {
296    Minimal,
297    Full,
298    Custom(i32), // Priority threshold
299}