aede_core/playlist/
selection.rs1use crate::model::{Catalog, Id};
3use std::collections::BTreeMap;
4use std::path::{Component, Path, PathBuf};
5
6fn normalized(path: &Path) -> Option<PathBuf> {
7 if !path.is_absolute() {
8 return None;
9 }
10 let mut result = PathBuf::new();
11 for part in path.components() {
12 match part {
13 Component::CurDir => {}
14 Component::ParentDir => {
15 if !result.pop() {
16 return None;
17 }
18 }
19 other => result.push(other.as_os_str()),
20 }
21 }
22 Some(result)
23}
24
25pub fn resolve(catalog: &Catalog, content: &str, parent: &Path) -> Result<Vec<Id>, String> {
29 if content.len() > 8 * 1024 * 1024 || content.contains('\0') || !parent.is_absolute() {
30 return Err("invalid or oversized M3U selection".into());
31 }
32 let mut paths: BTreeMap<PathBuf, Vec<Id>> = BTreeMap::new();
33 for track in &catalog.tracks {
34 if let Some(file) = catalog.file(track.file_id)
35 && let Some(path) = normalized(Path::new(&file.path))
36 {
37 paths.entry(path).or_default().push(track.id);
38 }
39 }
40 let mut tracks = Vec::new();
41 for (index, entry) in super::entry_lines(content) {
42 if tracks.len() >= 4096 {
43 return Err("M3U selection exceeds 4096 tracks".into());
44 }
45 if entry.contains("://")
47 || entry.starts_with("file:")
48 || entry.chars().any(char::is_control)
49 {
50 return Err(format!("M3U line {} is not a catalogued file", index + 1));
51 }
52 let path = normalized(&parent.join(entry))
53 .ok_or_else(|| format!("invalid M3U path on line {}", index + 1))?;
54 match paths.get(&path).map(Vec::as_slice) {
55 Some([track]) => tracks.push(*track),
56 _ => {
57 return Err(format!(
58 "M3U line {} has no unique catalogued track; synchronize the library",
59 index + 1
60 ));
61 }
62 }
63 }
64 if tracks.is_empty() {
65 return Err("M3U selection is empty".into());
66 }
67 Ok(tracks)
68}
69
70#[cfg(test)]
71#[path = "selection_tests.rs"]
72mod tests;