1use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use crate::config::DownloadOptions;
7use crate::error::{Error, Result};
8use crate::http::{DefaultHttpClient, HttpRequest, HttpResponse};
9use crate::manifest::ExtractorType;
10use crate::processor::{
11 ContentProcessor, DefaultDashContentProcessor, DefaultHlsContentProcessor, ParserConfig,
12};
13
14pub(crate) const HTTP_LIVE_TS_MARKER: &str = "<HAKI_LIVE_TS>";
15pub(crate) const BINARY_DATA_MARKER: &str = "<HAKI_BINARY_DATA>";
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum LoadedSourceKind {
20 Hls,
22 Dash,
24 Mss,
26 HttpLiveTs,
28 BinaryData,
30}
31
32impl LoadedSourceKind {
33 pub fn extractor_type(self) -> ExtractorType {
35 match self {
36 Self::Hls => ExtractorType::Hls,
37 Self::Dash => ExtractorType::MpegDash,
38 Self::Mss => ExtractorType::Mss,
39 Self::HttpLiveTs => ExtractorType::HttpLive,
40 Self::BinaryData => ExtractorType::HttpLive,
41 }
42 }
43
44 fn raw_file_name(self) -> &'static str {
45 match self {
46 Self::Hls => "raw.m3u8",
47 Self::Dash => "raw.mpd",
48 Self::Mss => "raw.ism",
49 Self::HttpLiveTs => "raw.txt",
50 Self::BinaryData => "raw.bin",
51 }
52 }
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct LoadedSource {
58 pub kind: LoadedSourceKind,
60 pub text: String,
62 pub original_url: String,
64 pub final_url: String,
66 pub raw_files: BTreeMap<String, String>,
68 pub debug_logs: Vec<String>,
70}
71
72pub struct SourceLoader {
74 http: DefaultHttpClient,
75 content_processors: Vec<Box<dyn ContentProcessor>>,
76}
77
78impl Default for SourceLoader {
79 fn default() -> Self {
80 Self {
81 http: DefaultHttpClient::new(),
82 content_processors: vec![
83 Box::<DefaultHlsContentProcessor>::default(),
84 Box::<DefaultDashContentProcessor>::default(),
85 ],
86 }
87 }
88}
89
90impl SourceLoader {
91 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn from_options(options: &DownloadOptions) -> Self {
98 Self::new().with_http(DefaultHttpClient::from_options(options))
99 }
100
101 pub fn with_http(mut self, http: DefaultHttpClient) -> Self {
103 self.http = http;
104 self
105 }
106
107 pub async fn load_source(
109 &self,
110 input: &str,
111 config: &mut ParserConfig,
112 ) -> Result<LoadedSource> {
113 let (mut text, original_url, final_url, mut debug_logs) = if input.starts_with("file:") {
114 let path = file_uri_to_path(input);
115 (
116 read_source_text_file(&path).await?,
117 input.to_string(),
118 input.to_string(),
119 Vec::new(),
120 )
121 } else if input.starts_with("http://") || input.starts_with("https://") {
122 let response = self.get_web_source(input, &config.headers).await?;
123 (response.0, input.to_string(), response.1, response.2)
124 } else if tokio::fs::metadata(input).await.is_ok() {
125 let path = tokio::fs::canonicalize(input).await?;
126 let file_uri =
127 path_to_file_uri(&path).unwrap_or_else(|| path.to_string_lossy().to_string());
128 (
129 read_source_text_file(&path).await?,
130 file_uri.clone(),
131 file_uri,
132 Vec::new(),
133 )
134 } else {
135 return Err(Error::http("source input could not be loaded"));
136 };
137
138 if text.trim().is_empty() {
139 return Err(Error::http("source input was empty"));
140 }
141 text = text.trim().trim_start_matches('\u{feff}').to_string();
142 config.original_url = original_url.clone();
143 config.url = final_url.clone();
144 let kind = detect_source_kind(&text)?;
145 let mut raw_files = BTreeMap::new();
146 raw_files.insert(kind.raw_file_name().to_string(), text.clone());
147 for processor in &self.content_processors {
148 if processor.can_process(kind.extractor_type(), &text, config) {
149 text = processor.process(&text, config)?;
150 }
151 }
152 debug_logs.retain(|line| !line.trim().is_empty());
153 Ok(LoadedSource {
154 kind,
155 text,
156 original_url,
157 final_url,
158 raw_files,
159 debug_logs,
160 })
161 }
162
163 pub async fn load_segment_bytes(&self, url: &str, config: &ParserConfig) -> Result<Vec<u8>> {
165 if url.starts_with("file:") {
166 return Ok(tokio::fs::read(file_uri_to_path(url)).await?);
167 }
168 if let Some(value) = url.strip_prefix("base64://") {
169 return base64_decode(value);
170 }
171 if let Some(value) = url.strip_prefix("hex://") {
172 return hex_to_bytes(value);
173 }
174 if tokio::fs::metadata(url).await.is_ok() {
175 return Ok(tokio::fs::read(url).await?);
176 }
177 let mut request = HttpRequest::new(url);
178 request.headers = config.headers.clone();
179 Ok(self.http.send(request).await?.body)
180 }
181
182 async fn get_web_source(
183 &self,
184 url: &str,
185 headers: &BTreeMap<String, String>,
186 ) -> Result<(String, String, Vec<String>)> {
187 let mut request = HttpRequest::new(url);
188 request.headers = headers.clone();
189 let response = self.http.send_source(request).await?;
190 let mut debug_logs = response.debug_logs.clone();
191 if is_mpeg2_ts_buffer(&response.body) {
192 debug_logs.push("Detected MPEG-TS stream".to_string());
193 return Ok((HTTP_LIVE_TS_MARKER.to_string(), url.to_string(), debug_logs));
194 }
195 if let Some(encoding) = response.headers.get("content-encoding")
196 && !encoding.trim().is_empty()
197 {
198 debug_logs.push(format!("Detected compression: {encoding}"));
199 }
200 let charset = response_charset(&response);
201 if charset.is_some() || has_text_bom(&response.body) {
202 let text = decode_text_bytes(&response.body, charset.as_deref())?;
203 return Ok((text, response.final_url, debug_logs));
204 }
205 if looks_like_binary(&sample(&response.body)) {
206 debug_logs.push("Heuristic detection: binary data".to_string());
207 return Ok((
208 BINARY_DATA_MARKER.to_string(),
209 response.final_url,
210 debug_logs,
211 ));
212 }
213 let text = decode_text_bytes(&response.body, charset.as_deref())?;
214 Ok((text, response.final_url, debug_logs))
215 }
216}
217
218async fn read_source_text_file(path: &std::path::Path) -> Result<String> {
219 decode_text_bytes(&tokio::fs::read(path).await?, None)
220}
221
222pub async fn write_raw_files(
224 raw_files: &BTreeMap<String, String>,
225 directory: &std::path::Path,
226) -> Result<Vec<PathBuf>> {
227 tokio::fs::create_dir_all(directory).await?;
228 let mut written = Vec::with_capacity(raw_files.len());
229 for (name, text) in raw_files {
230 let path = directory.join(name);
231 if tokio::fs::metadata(&path).await.is_err() {
232 tokio::fs::write(&path, n_utf8_text_file_bytes(text)).await?;
233 }
234 written.push(path);
235 }
236 Ok(written)
237}
238
239fn n_utf8_text_file_bytes(text: &str) -> Vec<u8> {
240 let mut bytes = Vec::with_capacity(3 + text.len());
241 bytes.extend_from_slice(&[0xef, 0xbb, 0xbf]);
242 bytes.extend_from_slice(text.as_bytes());
243 bytes
244}
245
246fn detect_source_kind(text: &str) -> Result<LoadedSourceKind> {
247 let trimmed = text.trim();
248 if trimmed.starts_with("#EXTM3U") {
249 Ok(LoadedSourceKind::Hls)
250 } else if trimmed.contains("</MPD>") && trimmed.contains("<MPD") {
251 Ok(LoadedSourceKind::Dash)
252 } else if trimmed.contains("</SmoothStreamingMedia>")
253 && trimmed.contains("<SmoothStreamingMedia")
254 {
255 Ok(LoadedSourceKind::Mss)
256 } else if trimmed == HTTP_LIVE_TS_MARKER {
257 Ok(LoadedSourceKind::HttpLiveTs)
258 } else if trimmed == BINARY_DATA_MARKER {
259 Ok(LoadedSourceKind::BinaryData)
260 } else {
261 Err(Error::compatibility("source input type is not supported"))
262 }
263}
264
265fn response_charset(response: &HttpResponse) -> Option<String> {
266 response
267 .headers
268 .get("content-type")
269 .and_then(|value| content_type_charset(value))
270}
271
272fn decode_text_bytes(bytes: &[u8], charset: Option<&str>) -> Result<String> {
273 if bytes.starts_with(&[0xef, 0xbb, 0xbf]) {
274 return Ok(String::from_utf8_lossy(&bytes[3..]).to_string());
275 }
276 if bytes.starts_with(&[0xff, 0xfe]) {
277 return decode_utf16(&bytes[2..], false);
278 }
279 if bytes.starts_with(&[0xfe, 0xff]) {
280 return decode_utf16(&bytes[2..], true);
281 }
282 match charset {
283 Some("utf-16") => decode_utf16_auto(bytes),
284 Some("utf-16le") => decode_utf16(bytes, false),
285 Some("utf-16be") => decode_utf16(bytes, true),
286 Some(label) => Ok(decode_with_label(label, bytes)),
287 None => Ok(String::from_utf8_lossy(bytes).to_string()),
288 }
289}
290
291fn has_text_bom(bytes: &[u8]) -> bool {
292 bytes.starts_with(&[0xef, 0xbb, 0xbf])
293 || bytes.starts_with(&[0xff, 0xfe])
294 || bytes.starts_with(&[0xfe, 0xff])
295}
296
297fn decode_with_label(label: &str, bytes: &[u8]) -> String {
298 match encoding_rs::Encoding::for_label(label.as_bytes()) {
299 Some(encoding) => {
300 let (text, _, _) = encoding.decode(bytes);
301 text.into_owned()
302 }
303 None => String::from_utf8_lossy(bytes).to_string(),
304 }
305}
306
307fn content_type_charset(content_type: &str) -> Option<String> {
308 content_type.split(';').find_map(|part| {
309 let (key, value) = part.trim().split_once('=')?;
310 if key.eq_ignore_ascii_case("charset") {
311 Some(value.trim_matches('"').to_ascii_lowercase())
312 } else {
313 None
314 }
315 })
316}
317
318fn decode_utf16_auto(bytes: &[u8]) -> Result<String> {
319 if bytes.starts_with(&[0xff, 0xfe]) {
320 return decode_utf16(&bytes[2..], false);
321 }
322 if bytes.starts_with(&[0xfe, 0xff]) {
323 return decode_utf16(&bytes[2..], true);
324 }
325 decode_utf16(bytes, false)
326}
327
328fn decode_utf16(bytes: &[u8], big_endian: bool) -> Result<String> {
329 let mut units = Vec::with_capacity(bytes.len() / 2);
330 for pair in bytes.chunks_exact(2) {
331 let unit = if big_endian {
332 u16::from_be_bytes([pair[0], pair[1]])
333 } else {
334 u16::from_le_bytes([pair[0], pair[1]])
335 };
336 units.push(unit);
337 }
338 let mut text = String::from_utf16_lossy(&units);
339 if bytes.len() & 1 != 0 {
340 text.push(char::REPLACEMENT_CHARACTER);
341 }
342 Ok(text)
343}
344
345fn sample(bytes: &[u8]) -> Vec<u8> {
346 bytes.iter().take(4096).copied().collect()
347}
348
349fn looks_like_binary(data: &[u8]) -> bool {
350 if data.is_empty() {
351 return false;
352 }
353 let mut non_text = 0_usize;
354 let mut total = 0_usize;
355 let mut index = 0_usize;
356 while index < data.len() {
357 let byte = data[index];
358 total += 1;
359 if byte == 0 {
360 return true;
361 }
362 if (0x20..=0x7e).contains(&byte) || matches!(byte, 0x09 | 0x0a | 0x0d) {
363 index += 1;
364 continue;
365 }
366 let seq_len = utf8_sequence_length(byte);
367 if seq_len > 1
368 && index + seq_len <= data.len()
369 && valid_utf8_sequence(&data[index..index + seq_len])
370 {
371 index += seq_len;
372 continue;
373 }
374 non_text += 1;
375 index += 1;
376 }
377 (non_text as f64 / total as f64) > 0.3
378}
379
380fn utf8_sequence_length(byte: u8) -> usize {
381 if byte & 0x80 == 0x00 {
382 1
383 } else if byte & 0xe0 == 0xc0 {
384 2
385 } else if byte & 0xf0 == 0xe0 {
386 3
387 } else if byte & 0xf8 == 0xf0 {
388 4
389 } else {
390 1
391 }
392}
393
394fn valid_utf8_sequence(seq: &[u8]) -> bool {
395 if seq.len() <= 1 {
396 return false;
397 }
398 seq.iter().skip(1).all(|byte| byte & 0xc0 == 0x80)
399}
400
401fn is_mpeg2_ts_buffer(buffer: &[u8]) -> bool {
402 const PACKET_SIZE: usize = 188;
403 if buffer.len() < PACKET_SIZE {
404 return false;
405 }
406 let packet_count = std::cmp::min(buffer.len() / PACKET_SIZE, 5);
407 let sync_count = (0..packet_count)
408 .filter(|index| buffer[index * PACKET_SIZE] == 0x47)
409 .count();
410 sync_count >= 3
411}
412
413fn file_uri_to_path(value: &str) -> PathBuf {
414 if let Ok(url) = reqwest::Url::parse(value)
415 && url.scheme() == "file"
416 && let Ok(path) = url.to_file_path()
417 {
418 return path;
419 }
420 let stripped = value
421 .trim_start_matches("file://")
422 .trim_start_matches("file:");
423 #[cfg(windows)]
424 {
425 let mut normalized = stripped.trim_start_matches('/');
426 if let Some(rest) = normalized.strip_prefix("?/") {
427 normalized = rest;
428 }
429 PathBuf::from(normalized)
430 }
431 #[cfg(not(windows))]
432 {
433 PathBuf::from(stripped)
434 }
435}
436
437fn path_to_file_uri(path: &Path) -> Option<String> {
438 let text = path.to_str()?;
439 #[cfg(windows)]
440 {
441 let text = text.replace('\\', "/");
442 let normalized = text.strip_prefix("//?/").unwrap_or(&text);
443 Some(format!("file:///{normalized}"))
444 }
445 #[cfg(not(windows))]
446 {
447 Some(format!("file://{text}"))
448 }
449}
450
451fn hex_to_bytes(value: &str) -> Result<Vec<u8>> {
452 let value = value.trim();
453 let value = value
454 .strip_prefix("0x")
455 .or_else(|| value.strip_prefix("0X"))
456 .unwrap_or(value);
457 if value.len() & 1 != 0 {
458 return Err(Error::config("hex length must be even"));
459 }
460 let mut bytes = Vec::with_capacity(value.len() / 2);
461 let mut chars = value.chars();
462 while let Some(high) = chars.next() {
463 let low = chars
464 .next()
465 .ok_or_else(|| Error::config("hex length must be even"))?;
466 let high = hex_value(high).ok_or_else(|| Error::config("hex is invalid"))?;
467 let low = hex_value(low).ok_or_else(|| Error::config("hex is invalid"))?;
468 bytes.push((high << 4) | low);
469 }
470 Ok(bytes)
471}
472
473fn hex_value(ch: char) -> Option<u8> {
474 match ch {
475 '0'..='9' => Some(ch as u8 - b'0'),
476 'a'..='f' => Some(ch as u8 - b'a' + 10),
477 'A'..='F' => Some(ch as u8 - b'A' + 10),
478 _ => None,
479 }
480}
481
482fn base64_decode(value: &str) -> Result<Vec<u8>> {
483 crate::base64::decode_base64(value).map_err(Error::config)
484}