firefox_webdriver/driver/profile/
extensions.rs

1//! Firefox extension installation and management.
2//!
3//! Extensions can be provided in three formats:
4//!
5//! | Format | Description |
6//! |--------|-------------|
7//! | Unpacked | Directory containing `manifest.json` |
8//! | Packed | `.xpi` or `.zip` archive |
9//! | Base64 | Base64-encoded `.xpi` content |
10//!
11//! # Example
12//!
13//! ```
14//! use firefox_webdriver::driver::profile::ExtensionSource;
15//!
16//! // Unpacked directory
17//! let unpacked = ExtensionSource::unpacked("./extension");
18//!
19//! // Packed .xpi file
20//! let packed = ExtensionSource::packed("./extension.xpi");
21//!
22//! // Base64-encoded (useful for embedding)
23//! let base64 = ExtensionSource::base64("UEsDBBQ...");
24//! ```
25
26// ============================================================================
27// Imports
28// ============================================================================
29
30use std::path::PathBuf;
31
32// ============================================================================
33// ExtensionSource
34// ============================================================================
35
36/// Source location for a Firefox extension.
37///
38/// Extensions can be provided as unpacked directories, packed archives,
39/// or base64-encoded content.
40///
41/// # Examples
42///
43/// ```
44/// use firefox_webdriver::driver::profile::ExtensionSource;
45///
46/// // Unpacked directory
47/// let unpacked = ExtensionSource::unpacked("./extension");
48///
49/// // Packed .xpi file
50/// let packed = ExtensionSource::packed("./extension.xpi");
51///
52/// // Base64-encoded
53/// let base64 = ExtensionSource::base64("UEsDBBQ...");
54/// ```
55#[derive(Debug, Clone, PartialEq, Eq, Hash)]
56pub enum ExtensionSource {
57    /// Path to an unpacked extension directory.
58    Unpacked(PathBuf),
59
60    /// Path to a packed extension archive (.xpi or .zip).
61    Packed(PathBuf),
62
63    /// Base64-encoded extension content.
64    Base64(String),
65}
66
67// ============================================================================
68// ExtensionSource - Constructors
69// ============================================================================
70
71impl ExtensionSource {
72    /// Creates an unpacked extension source.
73    ///
74    /// # Arguments
75    ///
76    /// * `path` - Path to directory containing `manifest.json`
77    #[inline]
78    #[must_use]
79    pub fn unpacked(path: impl Into<PathBuf>) -> Self {
80        Self::Unpacked(path.into())
81    }
82
83    /// Creates a packed extension source.
84    ///
85    /// # Arguments
86    ///
87    /// * `path` - Path to `.xpi` or `.zip` file
88    #[inline]
89    #[must_use]
90    pub fn packed(path: impl Into<PathBuf>) -> Self {
91        Self::Packed(path.into())
92    }
93
94    /// Creates a base64-encoded extension source.
95    ///
96    /// Useful for embedding extensions in the binary.
97    ///
98    /// # Arguments
99    ///
100    /// * `data` - Base64-encoded `.xpi` content
101    #[inline]
102    #[must_use]
103    pub fn base64(data: impl Into<String>) -> Self {
104        Self::Base64(data.into())
105    }
106}
107
108// ============================================================================
109// ExtensionSource - Accessors
110// ============================================================================
111
112impl ExtensionSource {
113    /// Returns the path if this is a file-based source.
114    ///
115    /// Returns `None` for base64-encoded sources.
116    #[inline]
117    #[must_use]
118    pub fn path(&self) -> Option<&PathBuf> {
119        match self {
120            Self::Unpacked(path) | Self::Packed(path) => Some(path),
121            Self::Base64(_) => None,
122        }
123    }
124
125    /// Returns `true` if this is an unpacked extension.
126    #[inline]
127    #[must_use]
128    pub fn is_unpacked(&self) -> bool {
129        matches!(self, Self::Unpacked(_))
130    }
131
132    /// Returns `true` if this is a packed extension.
133    #[inline]
134    #[must_use]
135    pub fn is_packed(&self) -> bool {
136        matches!(self, Self::Packed(_))
137    }
138
139    /// Returns `true` if this is a base64-encoded extension.
140    #[inline]
141    #[must_use]
142    pub fn is_base64(&self) -> bool {
143        matches!(self, Self::Base64(_))
144    }
145}
146
147// ============================================================================
148// Trait Implementations
149// ============================================================================
150
151impl From<PathBuf> for ExtensionSource {
152    /// Automatically determines extension type based on path.
153    ///
154    /// - Directories become [`ExtensionSource::Unpacked`]
155    /// - Files become [`ExtensionSource::Packed`]
156    fn from(path: PathBuf) -> Self {
157        if path.is_dir() {
158            Self::Unpacked(path)
159        } else {
160            Self::Packed(path)
161        }
162    }
163}
164
165impl From<&str> for ExtensionSource {
166    fn from(path: &str) -> Self {
167        Self::from(PathBuf::from(path))
168    }
169}
170
171impl From<String> for ExtensionSource {
172    fn from(path: String) -> Self {
173        Self::from(PathBuf::from(path))
174    }
175}
176
177// ============================================================================
178// Tests
179// ============================================================================
180
181#[cfg(test)]
182mod tests {
183    use super::ExtensionSource;
184
185    use std::path::PathBuf;
186
187    #[test]
188    fn test_unpacked_constructor() {
189        let source = ExtensionSource::unpacked("./extension");
190        assert!(source.is_unpacked());
191        assert!(!source.is_packed());
192        assert!(!source.is_base64());
193    }
194
195    #[test]
196    fn test_packed_constructor() {
197        let source = ExtensionSource::packed("./extension.xpi");
198        assert!(source.is_packed());
199        assert!(!source.is_unpacked());
200        assert!(!source.is_base64());
201    }
202
203    #[test]
204    fn test_base64_constructor() {
205        let source = ExtensionSource::base64("UEsDBBQ...");
206        assert!(source.is_base64());
207        assert!(!source.is_unpacked());
208        assert!(!source.is_packed());
209        assert!(source.path().is_none());
210    }
211
212    #[test]
213    fn test_path_accessor() {
214        let unpacked = ExtensionSource::unpacked("./ext");
215        assert_eq!(unpacked.path(), Some(&PathBuf::from("./ext")));
216
217        let packed = ExtensionSource::packed("./ext.xpi");
218        assert_eq!(packed.path(), Some(&PathBuf::from("./ext.xpi")));
219
220        let base64 = ExtensionSource::base64("data");
221        assert_eq!(base64.path(), None);
222    }
223
224    #[test]
225    fn test_from_pathbuf_directory() {
226        // Current directory is always a directory
227        let source = ExtensionSource::from(PathBuf::from("."));
228        assert!(source.is_unpacked());
229    }
230
231    #[test]
232    fn test_from_pathbuf_file() {
233        // Non-existent path treated as file
234        let source = ExtensionSource::from(PathBuf::from("./nonexistent.xpi"));
235        assert!(source.is_packed());
236    }
237
238    #[test]
239    fn test_from_str() {
240        let source = ExtensionSource::from("./extension");
241        assert!(source.path().is_some());
242    }
243
244    #[test]
245    fn test_from_string() {
246        let source = ExtensionSource::from(String::from("./extension"));
247        assert!(source.path().is_some());
248    }
249
250    #[test]
251    fn test_clone() {
252        let source = ExtensionSource::unpacked("./ext");
253        let cloned = source.clone();
254        assert_eq!(source, cloned);
255    }
256
257    #[test]
258    fn test_debug() {
259        let source = ExtensionSource::unpacked("./ext");
260        let debug_str = format!("{:?}", source);
261        assert!(debug_str.contains("Unpacked"));
262    }
263}