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}