1pub(crate) mod device;
2#[cfg(feature = "emulation-rand")]
3mod rand;
4
5use device::{chrome::*, firefox::*, okhttp::*, opera::*, safari::*};
6#[cfg(feature = "emulation-serde")]
7use serde::{Deserialize, Serialize};
8#[cfg(feature = "emulation-rand")]
9use strum_macros::VariantArray;
10use typed_builder::TypedBuilder;
11
12macro_rules! define_enum {
13 (
14 $(#[$meta:meta])*
15 with_dispatch,
16 $name:ident, $default_variant:ident,
17 $(
18 $variant:ident => ($rename:expr, $emulation_fn:path)
19 ),* $(,)?
20 ) => {
21 $(#[$meta])*
22 #[non_exhaustive]
23 #[derive(Clone, Copy, Hash, Debug, PartialEq, Eq)]
24 #[cfg_attr(feature = "emulation-rand", derive(VariantArray))]
25 #[cfg_attr(feature = "emulation-serde", derive(Deserialize, Serialize))]
26 pub enum $name {
27 $(
28 #[cfg_attr(feature = "emulation-serde", serde(rename = $rename))]
29 $variant,
30 )*
31 }
32
33 impl Default for $name {
34 fn default() -> Self {
35 $name::$default_variant
36 }
37 }
38
39 impl $name {
40 pub fn into_emulation(self, opt: EmulationOption) -> hpx::Emulation {
41 match self {
42 $(
43 $name::$variant => $emulation_fn(opt),
44 )*
45 }
46 }
47 }
48 };
49
50 (
51 $(#[$meta:meta])*
52 plain,
53 $name:ident, $default_variant:ident,
54 $(
55 $variant:ident => $rename:expr
56 ),* $(,)?
57 ) => {
58 $(#[$meta])*
59 #[non_exhaustive]
60 #[derive(Clone, Copy, Hash, Debug, PartialEq, Eq)]
61 #[cfg_attr(feature = "emulation-rand", derive(VariantArray))]
62 #[cfg_attr(feature = "emulation-serde", derive(Deserialize, Serialize))]
63 pub enum $name {
64 $(
65 #[cfg_attr(feature = "emulation-serde", serde(rename = $rename))]
66 $variant,
67 )*
68 }
69
70 impl Default for $name {
71 fn default() -> Self {
72 $name::$default_variant
73 }
74 }
75 };
76}
77
78define_enum!(
79 with_dispatch,
94 Emulation, Chrome143,
95
96 Chrome100 => ("chrome_100", v100::emulation),
98 Chrome101 => ("chrome_101", v101::emulation),
99 Chrome104 => ("chrome_104", v104::emulation),
100 Chrome105 => ("chrome_105", v105::emulation),
101 Chrome106 => ("chrome_106", v106::emulation),
102 Chrome107 => ("chrome_107", v107::emulation),
103 Chrome108 => ("chrome_108", v108::emulation),
104 Chrome109 => ("chrome_109", v109::emulation),
105 Chrome110 => ("chrome_110", v110::emulation),
106 Chrome114 => ("chrome_114", v114::emulation),
107 Chrome116 => ("chrome_116", v116::emulation),
108 Chrome117 => ("chrome_117", v117::emulation),
109 Chrome118 => ("chrome_118", v118::emulation),
110 Chrome119 => ("chrome_119", v119::emulation),
111 Chrome120 => ("chrome_120", v120::emulation),
112 Chrome123 => ("chrome_123", v123::emulation),
113 Chrome124 => ("chrome_124", v124::emulation),
114 Chrome126 => ("chrome_126", v126::emulation),
115 Chrome127 => ("chrome_127", v127::emulation),
116 Chrome128 => ("chrome_128", v128::emulation),
117 Chrome129 => ("chrome_129", v129::emulation),
118 Chrome130 => ("chrome_130", v130::emulation),
119 Chrome131 => ("chrome_131", v131::emulation),
120 Chrome132 => ("chrome_132", v132::emulation),
121 Chrome133 => ("chrome_133", v133::emulation),
122 Chrome134 => ("chrome_134", v134::emulation),
123 Chrome135 => ("chrome_135", v135::emulation),
124 Chrome136 => ("chrome_136", v136::emulation),
125 Chrome137 => ("chrome_137", v137::emulation),
126 Chrome138 => ("chrome_138", v138::emulation),
127 Chrome139 => ("chrome_139", v139::emulation),
128 Chrome140 => ("chrome_140", v140::emulation),
129 Chrome141 => ("chrome_141", v141::emulation),
130 Chrome142 => ("chrome_142", v142::emulation),
131 Chrome143 => ("chrome_143", v143::emulation),
132 Chrome144 => ("chrome_144", v144::emulation),
133 Chrome145 => ("chrome_145", v145::emulation),
134 Chrome146 => ("chrome_146", v146::emulation),
135 Chrome147 => ("chrome_147", v147::emulation),
136 Chrome148 => ("chrome_148", v148::emulation),
137 Chrome149 => ("chrome_149", v149::emulation),
138
139 Edge101 => ("edge_101", edge101::emulation),
141 Edge122 => ("edge_122", edge122::emulation),
142 Edge127 => ("edge_127", edge127::emulation),
143 Edge131 => ("edge_131", edge131::emulation),
144 Edge134 => ("edge_134", edge134::emulation),
145 Edge135 => ("edge_135", edge135::emulation),
146 Edge136 => ("edge_136", edge136::emulation),
147 Edge137 => ("edge_137", edge137::emulation),
148 Edge138 => ("edge_138", edge138::emulation),
149 Edge139 => ("edge_139", edge139::emulation),
150 Edge140 => ("edge_140", edge140::emulation),
151 Edge141 => ("edge_141", edge141::emulation),
152 Edge142 => ("edge_142", edge142::emulation),
153 Edge143 => ("edge_143", edge143::emulation),
154 Edge144 => ("edge_144", edge144::emulation),
155 Edge145 => ("edge_145", edge145::emulation),
156 Edge146 => ("edge_146", edge146::emulation),
157 Edge147 => ("edge_147", edge147::emulation),
158 Edge148 => ("edge_148", edge148::emulation),
159
160 Opera116 => ("opera_116", opera116::emulation),
162 Opera117 => ("opera_117", opera117::emulation),
163 Opera118 => ("opera_118", opera118::emulation),
164 Opera119 => ("opera_119", opera119::emulation),
165 Opera120 => ("opera_120", opera120::emulation),
166 Opera121 => ("opera_121", opera121::emulation),
167 Opera122 => ("opera_122", opera122::emulation),
168 Opera123 => ("opera_123", opera123::emulation),
169 Opera124 => ("opera_124", opera124::emulation),
170 Opera125 => ("opera_125", opera125::emulation),
171 Opera126 => ("opera_126", opera126::emulation),
172 Opera127 => ("opera_127", opera127::emulation),
173 Opera128 => ("opera_128", opera128::emulation),
174 Opera129 => ("opera_129", opera129::emulation),
175 Opera130 => ("opera_130", opera130::emulation),
176 Opera131 => ("opera_131", opera131::emulation),
177
178 SafariIos17_2 => ("safari_ios_17.2", safari_ios_17_2::emulation),
180 SafariIos17_4_1 => ("safari_ios_17.4.1", safari_ios_17_4_1::emulation),
181 SafariIos16_5 => ("safari_ios_16.5", safari_ios_16_5::emulation),
182 Safari15_3 => ("safari_15.3", safari15_3::emulation),
183 Safari15_5 => ("safari_15.5", safari15_5::emulation),
184 Safari15_6_1 => ("safari_15.6.1", safari15_6_1::emulation),
185 Safari16 => ("safari_16", safari16::emulation),
186 Safari16_5 => ("safari_16.5", safari16_5::emulation),
187 Safari17_0 => ("safari_17.0", safari17_0::emulation),
188 Safari17_2_1 => ("safari_17.2.1", safari17_2_1::emulation),
189 Safari17_4_1 => ("safari_17.4.1", safari17_4_1::emulation),
190 Safari17_5 => ("safari_17.5", safari17_5::emulation),
191 Safari17_6 => ("safari_17.6", safari17_6::emulation),
192 Safari18 => ("safari_18", safari18::emulation),
193 SafariIPad18 => ("safari_ipad_18", safari_ipad_18::emulation),
194 Safari18_2 => ("safari_18.2", safari18_2::emulation),
195 SafariIos18_1_1 => ("safari_ios_18.1.1", safari_ios_18_1_1::emulation),
196 Safari18_3 => ("safari_18.3", safari18_3::emulation),
197 Safari18_3_1 => ("safari_18.3.1", safari18_3_1::emulation),
198 Safari18_5 => ("safari_18.5", safari18_5::emulation),
199 Safari26 => ("safari_26", safari26::emulation),
200 Safari26_1 => ("safari_26.1", safari26_1::emulation),
201 Safari26_2 => ("safari_26.2", safari26_2::emulation),
202 SafariIPad26 => ("safari_ipad_26", safari_ipad_26::emulation),
203 SafariIPad26_2 => ("safari_ipad_26.2", safari_ipad_26_2::emulation),
204 SafariIos26 => ("safari_ios_26", safari_ios_26::emulation),
205 SafariIos26_2 => ("safari_ios_26.2", safari_ios_26_2::emulation),
206 Safari19 => ("safari_19", safari19::emulation),
207 SafariIos19 => ("safari_ios_19", safari_ios_19::emulation),
208 SafariIPad19 => ("safari_ipad_19", safari_ipad_19::emulation),
209 Safari20 => ("safari_20", safari20::emulation),
210 SafariIos20 => ("safari_ios_20", safari_ios_20::emulation),
211 SafariIPad20 => ("safari_ipad_20", safari_ipad_20::emulation),
212 Safari21 => ("safari_21", safari21::emulation),
213 SafariIos21 => ("safari_ios_21", safari_ios_21::emulation),
214 SafariIPad21 => ("safari_ipad_21", safari_ipad_21::emulation),
215 Safari22 => ("safari_22", safari22::emulation),
216 SafariIos22 => ("safari_ios_22", safari_ios_22::emulation),
217 SafariIPad22 => ("safari_ipad_22", safari_ipad_22::emulation),
218 Safari23 => ("safari_23", safari23::emulation),
219 SafariIos23 => ("safari_ios_23", safari_ios_23::emulation),
220 SafariIPad23 => ("safari_ipad_23", safari_ipad_23::emulation),
221 Safari24 => ("safari_24", safari24::emulation),
222 SafariIos24 => ("safari_ios_24", safari_ios_24::emulation),
223 SafariIPad24 => ("safari_ipad_24", safari_ipad_24::emulation),
224 Safari25 => ("safari_25", safari25::emulation),
225 SafariIos25 => ("safari_ios_25", safari_ios_25::emulation),
226 SafariIPad25 => ("safari_ipad_25", safari_ipad_25::emulation),
227 Safari26_3 => ("safari_26.3", safari26_3::emulation),
228 SafariIos26_3 => ("safari_ios_26.3", safari_ios_26_3::emulation),
229 SafariIPad26_3 => ("safari_ipad_26.3", safari_ipad_26_3::emulation),
230 Safari26_4 => ("safari_26.4", safari26_4::emulation),
231 SafariIos26_4 => ("safari_ios_26.4", safari_ios_26_4::emulation),
232 SafariIPad26_4 => ("safari_ipad_26.4", safari_ipad_26_4::emulation),
233
234 Firefox109 => ("firefox_109", ff109::emulation),
236 Firefox117 => ("firefox_117", ff117::emulation),
237 Firefox128 => ("firefox_128", ff128::emulation),
238 Firefox133 => ("firefox_133", ff133::emulation),
239 Firefox135 => ("firefox_135", ff135::emulation),
240 FirefoxPrivate135 => ("firefox_private_135", ff_private_135::emulation),
241 FirefoxAndroid135 => ("firefox_android_135", ff_android_135::emulation),
242 Firefox136 => ("firefox_136", ff136::emulation),
243 FirefoxPrivate136 => ("firefox_private_136", ff_private_136::emulation),
244 Firefox137 => ("firefox_137", ff137::emulation),
245 Firefox138 => ("firefox_138", ff138::emulation),
246 Firefox139 => ("firefox_139", ff139::emulation),
247 Firefox140 => ("firefox_140", ff140::emulation),
248 Firefox141 => ("firefox_141", ff141::emulation),
249 Firefox142 => ("firefox_142", ff142::emulation),
250 Firefox143 => ("firefox_143", ff143::emulation),
251 Firefox144 => ("firefox_144", ff144::emulation),
252 Firefox145 => ("firefox_145", ff145::emulation),
253 Firefox146 => ("firefox_146", ff146::emulation),
254 Firefox147 => ("firefox_147", ff147::emulation),
255 Firefox148 => ("firefox_148", ff148::emulation),
256 Firefox149 => ("firefox_149", ff149::emulation),
257 Firefox150 => ("firefox_150", ff150::emulation),
258 Firefox151 => ("firefox_151", ff151::emulation),
259
260 OkHttp3_9 => ("okhttp_3.9", okhttp3_9::emulation),
262 OkHttp3_11 => ("okhttp_3.11", okhttp3_11::emulation),
263 OkHttp3_13 => ("okhttp_3.13", okhttp3_13::emulation),
264 OkHttp3_14 => ("okhttp_3.14", okhttp3_14::emulation),
265 OkHttp4_9 => ("okhttp_4.9", okhttp4_9::emulation),
266 OkHttp4_10 => ("okhttp_4.10", okhttp4_10::emulation),
267 OkHttp4_12 => ("okhttp_4.12", okhttp4_12::emulation),
268 OkHttp5 => ("okhttp_5", okhttp5::emulation)
269
270);
271
272impl hpx::EmulationFactory for Emulation {
274 #[inline]
275 fn emulation(self) -> hpx::Emulation {
276 EmulationOption::builder()
277 .emulation(self)
278 .build()
279 .emulation()
280 }
281}
282
283define_enum!(
284 plain,
297 EmulationOS, MacOS,
298 Windows => "windows",
299 MacOS => "macos",
300 Linux => "linux",
301 Android => "android",
302 IOS => "ios"
303);
304
305define_enum!(
306 plain,
311 Platform, Windows,
312 Windows => "windows",
313 MacOS => "macos",
314 Linux => "linux",
315 Android => "android",
316 IOS => "ios"
317);
318
319impl Platform {
320 #[inline]
322 pub const fn is_mobile(&self) -> bool {
323 matches!(self, Platform::Android | Platform::IOS)
324 }
325
326 #[inline]
328 pub const fn sec_ch_ua_platform(self) -> &'static str {
329 match self {
330 Self::Windows => "\"Windows\"",
331 Self::MacOS => "\"macOS\"",
332 Self::Linux => "\"Linux\"",
333 Self::Android => "\"Android\"",
334 Self::IOS => "\"iOS\"",
335 }
336 }
337}
338
339impl From<Platform> for EmulationOS {
340 #[inline]
341 fn from(platform: Platform) -> Self {
342 match platform {
343 Platform::Windows => Self::Windows,
344 Platform::MacOS => Self::MacOS,
345 Platform::Linux => Self::Linux,
346 Platform::Android => Self::Android,
347 Platform::IOS => Self::IOS,
348 }
349 }
350}
351
352impl EmulationOS {
354 #[inline]
355 const fn platform(&self) -> &'static str {
356 match self {
357 EmulationOS::MacOS => "\"macOS\"",
358 EmulationOS::Linux => "\"Linux\"",
359 EmulationOS::Windows => "\"Windows\"",
360 EmulationOS::Android => "\"Android\"",
361 EmulationOS::IOS => "\"iOS\"",
362 }
363 }
364
365 #[inline]
366 const fn is_mobile(&self) -> bool {
367 matches!(self, EmulationOS::Android | EmulationOS::IOS)
368 }
369}
370
371#[derive(Default, Clone, Debug, TypedBuilder)]
387pub struct EmulationOption {
388 #[builder(default)]
390 emulation: Emulation,
391
392 #[builder(default)]
394 emulation_os: EmulationOS,
395
396 #[builder(default)]
398 platform: Platform,
399
400 #[builder(default = false)]
402 skip_http2: bool,
403
404 #[builder(default = false)]
406 skip_headers: bool,
407}
408
409impl hpx::EmulationFactory for EmulationOption {
411 #[inline]
412 fn emulation(mut self) -> hpx::Emulation {
413 if self.platform != Platform::Windows {
417 self.emulation_os = EmulationOS::from(self.platform);
418 }
419 self.emulation.into_emulation(self)
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use hpx::EmulationFactory;
426
427 use super::*;
428
429 #[test]
430 fn platform_is_mobile() {
431 assert!(!Platform::Windows.is_mobile());
432 assert!(!Platform::MacOS.is_mobile());
433 assert!(!Platform::Linux.is_mobile());
434 assert!(Platform::Android.is_mobile());
435 assert!(Platform::IOS.is_mobile());
436 }
437
438 #[test]
439 fn platform_to_emulation_os() {
440 assert!(matches!(
441 EmulationOS::from(Platform::Windows),
442 EmulationOS::Windows
443 ));
444 assert!(matches!(
445 EmulationOS::from(Platform::MacOS),
446 EmulationOS::MacOS
447 ));
448 assert!(matches!(
449 EmulationOS::from(Platform::Linux),
450 EmulationOS::Linux
451 ));
452 assert!(matches!(
453 EmulationOS::from(Platform::Android),
454 EmulationOS::Android
455 ));
456 assert!(matches!(EmulationOS::from(Platform::IOS), EmulationOS::IOS));
457 }
458
459 #[test]
460 fn emulation_option_builder_with_platform() {
461 let option = EmulationOption::builder().platform(Platform::Linux).build();
462 assert!(matches!(option.platform, Platform::Linux));
463 }
464
465 #[test]
466 fn platform_affects_emulation_output() {
467 let mut emu = EmulationOption::builder()
468 .emulation(Emulation::Chrome147)
469 .platform(Platform::Linux)
470 .build()
471 .emulation();
472 let ua = emu
473 .headers_mut()
474 .get(http::header::USER_AGENT)
475 .unwrap()
476 .to_str()
477 .unwrap();
478 assert!(
479 ua.contains("Linux"),
480 "expected Linux in User-Agent, got: {ua}"
481 );
482 assert!(
483 !ua.contains("Macintosh"),
484 "did not expect Macintosh in User-Agent, got: {ua}"
485 );
486 }
487
488 #[test]
489 #[cfg(feature = "emulation-rand")]
490 fn variant_count_at_least_120() {
491 use strum::VariantArray;
492 assert!(
493 Emulation::VARIANTS.len() >= 120,
494 "Expected at least 120 Emulation variants, found {}",
495 Emulation::VARIANTS.len()
496 );
497 }
498
499 #[test]
500 fn platform_sec_ch_ua_platform() {
501 assert_eq!(Platform::Linux.sec_ch_ua_platform(), "\"Linux\"");
502 assert_eq!(Platform::Windows.sec_ch_ua_platform(), "\"Windows\"");
503 assert_eq!(Platform::MacOS.sec_ch_ua_platform(), "\"macOS\"");
504 assert_eq!(Platform::Android.sec_ch_ua_platform(), "\"Android\"");
505 assert_eq!(Platform::IOS.sec_ch_ua_platform(), "\"iOS\"");
506 }
507
508 #[test]
509 fn explicit_emulation_os_preserved_when_platform_default() {
510 let mut em = EmulationOption::builder()
513 .emulation(Emulation::Chrome147)
514 .emulation_os(EmulationOS::Linux)
515 .build()
516 .emulation();
517 let ua = em
518 .headers_mut()
519 .get(http::header::USER_AGENT)
520 .unwrap()
521 .to_str()
522 .unwrap();
523 assert!(
524 ua.contains("Linux"),
525 "expected Linux in User-Agent when emulation_os=Linux, got: {ua}"
526 );
527 }
528}