1mod audio;
2mod buffer;
3mod cdm;
4mod eme;
5mod http_cache;
6mod media;
7mod mp4;
8mod net;
9mod player;
10mod runtime;
11
12use std::path::PathBuf;
13
14#[cfg(feature = "diagnostics")]
15pub use player::Diagnostics;
16pub use player::{
17 COMMAND_WAIT, Command, PlayState, Player, PlayerConfig, PlayerError, QUEUE_SHOWN, Queue,
18 QueueItem, Repeat, STOREFRONT_BACKOFF, STOREFRONT_DEADLINE, Snapshot, Tokens,
19};
20pub use runtime::Engine;
21
22#[cfg(feature = "sabotage")]
23#[used]
24static SABOTAGE_MARKER: &str = "bramley-unshipped-feature:engine/sabotage\0";
25#[cfg(feature = "diagnostics")]
26#[used]
27static DIAGNOSTICS_MARKER: &str = "bramley-unshipped-feature:engine/diagnostics\0";
28
29#[cfg(feature = "trace")]
30#[used]
31static TRACE_MARKER: &str = "bramley-unshipped-feature:engine/trace\0";
32
33#[used]
34static LINKED_MARKER: &str = "bramley-linked:engine\0";
35
36pub fn user_agent() -> String {
37 format!(
38 "Bramley/{} ({})",
39 env!("CARGO_PKG_VERSION"),
40 std::env::consts::OS
41 )
42}
43
44#[derive(Clone)]
45#[non_exhaustive]
46pub struct EngineConfig {
47 pub cdm_path: PathBuf,
48 pub audio_sink_name: String,
49 pub output_device: Option<String>,
50 pub redact: Vec<String>,
51}
52
53impl EngineConfig {
54 pub fn default_cdm_path() -> PathBuf {
55 if cfg!(target_os = "linux") {
56 PathBuf::from(
57 "/opt/google/chrome/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so",
58 )
59 } else {
60 PathBuf::from(format!(
61 "{}widevinecdm{}",
62 std::env::consts::DLL_PREFIX,
63 std::env::consts::DLL_SUFFIX
64 ))
65 }
66 }
67}
68
69impl std::fmt::Debug for EngineConfig {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("EngineConfig")
72 .field("cdm_path", &self.cdm_path)
73 .field("audio_sink_name", &self.audio_sink_name)
74 .field("output_device", &self.output_device)
75 .finish_non_exhaustive()
76 }
77}
78
79impl Default for EngineConfig {
80 fn default() -> Self {
81 EngineConfig {
82 cdm_path: Self::default_cdm_path(),
83 audio_sink_name: "engine".into(),
84 output_device: None,
85 redact: Vec::new(),
86 }
87 }
88}
89
90pub fn redact(text: &str, secrets: &[String]) -> String {
91 let mut out = text.to_string();
92 for secret in secrets.iter().filter(|s| !s.is_empty()) {
93 let encoded = secret
94 .replace('+', "%2B")
95 .replace('/', "%2F")
96 .replace('=', "%3D");
97 mask_from_prefix(&mut out, &[secret, &encoded]);
98 }
99 let jwt_char = |c: char| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.');
100 let mut from = 0;
101 while let Some(i) = out[from..].find("eyJ").map(|i| i + from) {
102 let end = out[i..].find(|c| !jwt_char(c)).map_or(out.len(), |e| i + e);
103 if out[i..end].matches('.').count() >= 2 {
104 out.replace_range(i..end, "<jwt>");
105 from = i + 5;
106 } else {
107 from = end;
108 }
109 }
110 out
111}
112
113fn mask_from_prefix(text: &mut String, forms: &[&str]) {
114 const MIN_PREFIX: usize = 16;
115 for head in forms.iter().map(|f| f.get(..MIN_PREFIX).unwrap_or(f)) {
116 let mut from = 0;
117 while let Some(i) = text[from..].find(head).map(|i| i + from) {
118 let len = forms
119 .iter()
120 .map(|f| {
121 text[i..]
122 .bytes()
123 .zip(f.bytes())
124 .take_while(|(a, b)| a == b)
125 .count()
126 })
127 .max()
128 .unwrap_or(0);
129 text.replace_range(i..i + len, "<redacted>");
130 from = i + "<redacted>".len();
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests;