openpack/types.rs
1use memmap2::Mmap;
2use std::fmt::{self, Display};
3use std::io;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[cfg(not(any(feature = "zip", feature = "apk", feature = "crx", feature = "ipa")))]
10compile_error!("openpack needs at least one feature enabled");
11
12/// Archive format detected from path extension or CRX magic bytes.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ArchiveFormat {
15 /// Standard ZIP archive format.
16 Zip,
17 /// Java Archive (JAR) format - treated as ZIP.
18 Jar,
19 /// Android Application Package (APK) format.
20 Apk,
21 /// iOS App Store Package (IPA) format.
22 Ipa,
23 /// Chrome Extension (CRX) format.
24 Crx,
25}
26
27impl Display for ArchiveFormat {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 let value = match self {
30 Self::Zip => "zip",
31 Self::Jar => "jar",
32 Self::Apk => "apk",
33 Self::Ipa => "ipa",
34 Self::Crx => "crx",
35 };
36 write!(f, "{value}")
37 }
38}
39
40/// Safety guardrails for archive size and expansion limits.
41///
42/// These limits protect against zip bombs, resource exhaustion, and other
43/// denial-of-service attacks via malicious archives.
44///
45/// # Examples
46///
47/// ```
48/// use openpack::Limits;
49///
50/// // Use default limits for most use cases
51/// let limits = Limits::default();
52/// assert!(limits.max_archive_size > 0);
53/// ```
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct Limits {
56 /// Maximum size of the archive file itself in bytes.
57 ///
58 /// Default: 256 MiB
59 pub max_archive_size: u64,
60 /// Maximum uncompressed size of any single entry in bytes.
61 ///
62 /// Default: 50 MiB
63 pub max_entry_uncompressed_size: u64,
64 /// Maximum total uncompressed size of all entries combined in bytes.
65 ///
66 /// Default: 128 MiB
67 pub max_total_uncompressed_size: u64,
68 /// Maximum number of entries allowed in the archive.
69 ///
70 /// Default: 2048
71 pub max_entries: usize,
72 /// Maximum compression ratio (uncompressed / compressed) allowed.
73 /// Higher ratios may indicate zip bombs.
74 ///
75 /// Default: 100.0
76 pub max_compression_ratio: f64,
77}
78
79impl Default for Limits {
80 fn default() -> Self {
81 Self {
82 max_archive_size: 256 * 1024 * 1024,
83 max_entry_uncompressed_size: 50 * 1024 * 1024,
84 max_total_uncompressed_size: 128 * 1024 * 1024,
85 max_entries: 2048,
86 max_compression_ratio: 100.0,
87 }
88 }
89}
90
91/// Metadata for a single entry (file or directory) within an archive.
92///
93/// This struct contains information about an archive entry without
94/// the actual file data. Use [`OpenPack::read_entry`](crate::OpenPack::read_entry)
95/// to read the entry's contents.
96///
97/// # Examples
98///
99/// ```
100/// use openpack::OpenPack;
101///
102/// # fn example(pack: OpenPack) -> Result<(), Box<dyn std::error::Error>> {
103/// for entry in pack.entries()? {
104/// println!("{}: {} bytes", entry.name, entry.uncompressed_size);
105/// }
106/// # Ok(())
107/// # }
108/// ```
109#[derive(Debug, Clone, Default)]
110pub struct ArchiveEntry {
111 /// The entry's path name within the archive.
112 pub name: String,
113 /// Size of the entry's compressed data in bytes.
114 pub compressed_size: u64,
115 /// Size of the entry's uncompressed data in bytes.
116 pub uncompressed_size: u64,
117 /// CRC32 checksum of the uncompressed data.
118 pub crc: u32,
119 /// Whether this entry represents a directory.
120 pub is_dir: bool,
121}
122
123/// A handle to an opened archive file.
124///
125/// `OpenPack` provides safe access to ZIP-derived archives with built-in
126/// protection against Zip Slip, zip bombs, and other malicious archive
127/// structures.
128///
129/// The archive data is memory-mapped for efficient access, and all
130/// operations enforce the safety limits specified when opening.
131///
132/// # Examples
133///
134/// ```
135/// use openpack::{OpenPack, Limits};
136///
137/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
138/// // Open with default limits
139/// let pack = OpenPack::open_default("archive.zip")?;
140///
141/// // List all entries
142/// for entry in pack.entries()? {
143/// println!("Found: {}", entry.name);
144/// }
145///
146/// // Read a specific entry
147/// if pack.contains("readme.txt")? {
148/// let content = pack.read_entry("readme.txt")?;
149/// println!("Content: {:?}", String::from_utf8_lossy(&content));
150/// }
151/// # Ok(())
152/// # }
153/// ```
154#[derive(Debug)]
155pub struct OpenPack {
156 pub(crate) path: PathBuf,
157 pub(crate) mmap: Mmap,
158 pub(crate) format: ArchiveFormat,
159 pub(crate) limits: Limits,
160}
161
162/// High-signal summary fields from a browser extension `manifest.json`.
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct ExtensionManifestSummary {
165 /// Human-readable extension name.
166 pub name: Option<String>,
167 /// Version string.
168 pub version: Option<String>,
169 /// Manifest version.
170 pub manifest_version: Option<u64>,
171 /// Declared permissions.
172 pub permissions: Vec<String>,
173 /// Declared host permissions.
174 pub host_permissions: Vec<String>,
175 /// Background or service worker scripts.
176 pub background_scripts: Vec<String>,
177 /// Declared content script paths.
178 pub content_scripts: Vec<String>,
179}
180
181/// High-signal summary fields from `package.json`.
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct PackageJsonSummary {
184 /// Package name.
185 pub name: Option<String>,
186 /// Package version.
187 pub version: Option<String>,
188 /// Description field.
189 pub description: Option<String>,
190 /// Main entry point.
191 pub main: Option<String>,
192 /// Module entry point.
193 pub module: Option<String>,
194 /// Browser field when present.
195 pub browser: Option<String>,
196 /// Dependency names.
197 pub dependencies: Vec<String>,
198}
199
200/// Parsed Android manifest data from an APK file.
201///
202/// This struct contains key metadata extracted from the `AndroidManifest.xml`
203/// file within an APK archive.
204///
205/// Requires the `"apk"` feature to be enabled.
206///
207/// # Examples
208///
209/// ```
210/// use openpack::OpenPack;
211///
212/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
213/// # #[cfg(feature = "apk")]
214/// # {
215/// let pack = OpenPack::open_default("app.apk")?;
216/// let manifest = pack.read_android_manifest()?;
217/// println!("Package: {}", manifest.package);
218/// if let Some(version) = manifest.version_name {
219/// println!("Version: {}", version);
220/// }
221/// # }
222/// # Ok(())
223/// # }
224/// ```
225#[cfg(feature = "apk")]
226#[derive(Debug, Clone)]
227pub struct AndroidManifest {
228 /// The package name (e.g., "com.example.app").
229 pub package: String,
230 /// The human-readable version name (e.g., "1.2.3").
231 pub version_name: Option<String>,
232 /// The internal version code (e.g., "42").
233 pub version_code: Option<String>,
234 /// The minimum Android SDK version required.
235 pub min_sdk: Option<String>,
236}
237
238/// Parsed Info.plist data from an IPA file.
239///
240/// This struct contains key metadata extracted from the `Info.plist`
241/// file within an iOS app bundle in an IPA archive.
242///
243/// Requires the `"ipa"` feature to be enabled.
244///
245/// # Examples
246///
247/// ```
248/// use openpack::OpenPack;
249///
250/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
251/// # #[cfg(feature = "ipa")]
252/// # {
253/// let pack = OpenPack::open_default("app.ipa")?;
254/// let info = pack.read_info_plist()?;
255/// if let Some(bundle_id) = info.bundle_identifier {
256/// println!("Bundle ID: {}", bundle_id);
257/// }
258/// # }
259/// # Ok(())
260/// # }
261/// ```
262#[cfg(feature = "ipa")]
263#[derive(Debug, Clone)]
264pub struct IpaInfoPlist {
265 /// The bundle identifier (e.g., "com.example.MyApp").
266 pub bundle_identifier: Option<String>,
267 /// The bundle version string (e.g., "1.2.3").
268 pub bundle_version: Option<String>,
269 /// The name of the executable file.
270 pub executable: Option<String>,
271}
272
273/// Errors that can occur when working with archives.
274///
275/// This enum covers all failure modes when opening, inspecting, or
276/// extracting archive contents. Each variant includes a helpful message
277/// explaining what went wrong and how to fix it.
278#[derive(Error, Debug)]
279pub enum OpenPackError {
280 /// The provided configuration is invalid.
281 #[error("invalid openpack configuration: {0}. Fix: use positive limits and keep max archive and entry sizes consistent.")]
282 InvalidConfig(String),
283
284 /// An I/O error occurred while reading the archive.
285 #[error("archive I/O error: {0}. Fix: verify the archive path exists, is readable, and is not concurrently truncated.")]
286 Io(#[from] io::Error),
287
288 /// The ZIP format is invalid or unsupported.
289 #[error("ZIP parsing error: {0}. Fix: verify the file is a valid ZIP-derived archive and not truncated or encrypted in an unsupported way.")]
290 Zip(#[from] zip::result::ZipError),
291
292 /// The archive structure is malformed.
293 #[error("invalid archive structure: {0}. Fix: inspect the archive for malformed headers, invalid paths, or unsupported layout.")]
294 InvalidArchive(String),
295
296 /// A path traversal attack was detected (Zip Slip).
297 #[error("blocked suspicious archive entry `{0}` because it would escape the extraction root. Fix: remove path traversal segments like `../` from the archive.")]
298 ZipSlip(String),
299
300 /// The requested entry was not found in the archive.
301 #[error("archive entry `{0}` was not found. Fix: inspect `pack.entries()` first and use one of the returned entry names.")]
302 MissingEntry(String),
303
304 /// A safety limit was exceeded (size, count, or compression ratio).
305 #[error("archive safety limit exceeded: {0}. Fix: raise the relevant `Limits` value only if you trust the archive source.")]
306 LimitExceeded(String),
307
308 /// The archive format is not supported (feature not enabled).
309 #[error("unsupported archive format. Fix: use a ZIP, JAR, APK, IPA, or CRX file with the matching crate feature enabled.")]
310 Unsupported,
311}