Skip to main content

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}