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, 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)]
45pub struct EngineConfig {
46 pub cdm_path: PathBuf,
47 pub audio_sink_name: String,
48 pub output_device: Option<String>,
49 pub redact: Vec<String>,
50}
51
52impl EngineConfig {
53 pub fn default_cdm_path() -> PathBuf {
54 if cfg!(target_os = "linux") {
55 PathBuf::from(
56 "/opt/google/chrome/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so",
57 )
58 } else {
59 PathBuf::from(format!(
60 "{}widevinecdm{}",
61 std::env::consts::DLL_PREFIX,
62 std::env::consts::DLL_SUFFIX
63 ))
64 }
65 }
66}
67
68impl std::fmt::Debug for EngineConfig {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("EngineConfig")
71 .field("cdm_path", &self.cdm_path)
72 .field("audio_sink_name", &self.audio_sink_name)
73 .field("output_device", &self.output_device)
74 .finish_non_exhaustive()
75 }
76}
77
78impl Default for EngineConfig {
79 fn default() -> Self {
80 EngineConfig {
81 cdm_path: Self::default_cdm_path(),
82 audio_sink_name: "engine".into(),
83 output_device: None,
84 redact: Vec::new(),
85 }
86 }
87}
88
89pub fn redact(text: &str, secrets: &[String]) -> String {
90 let mut out = text.to_string();
91 for secret in secrets.iter().filter(|s| !s.is_empty()) {
92 let encoded = secret
93 .replace('+', "%2B")
94 .replace('/', "%2F")
95 .replace('=', "%3D");
96 mask_from_prefix(&mut out, &[secret, &encoded]);
97 }
98 let jwt_char = |c: char| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.');
99 let mut from = 0;
100 while let Some(i) = out[from..].find("eyJ").map(|i| i + from) {
101 let end = out[i..].find(|c| !jwt_char(c)).map_or(out.len(), |e| i + e);
102 if out[i..end].matches('.').count() >= 2 {
103 out.replace_range(i..end, "<jwt>");
104 from = i + 5;
105 } else {
106 from = end;
107 }
108 }
109 out
110}
111
112fn mask_from_prefix(text: &mut String, forms: &[&str]) {
113 const MIN_PREFIX: usize = 16;
114 for head in forms.iter().map(|f| f.get(..MIN_PREFIX).unwrap_or(f)) {
115 let mut from = 0;
116 while let Some(i) = text[from..].find(head).map(|i| i + from) {
117 let len = forms
118 .iter()
119 .map(|f| {
120 text[i..]
121 .bytes()
122 .zip(f.bytes())
123 .take_while(|(a, b)| a == b)
124 .count()
125 })
126 .max()
127 .unwrap_or(0);
128 text.replace_range(i..i + len, "<redacted>");
129 from = i + "<redacted>".len();
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests;