aede_core/playlist/files/
read.rs1use super::*;
4use std::io::Read;
5
6const MAX_BYTES: u64 = 8 * 1024 * 1024;
7const MAX_TRACKS: usize = 4096;
8
9pub struct Document {
11 pub file: File,
13 pub content: String,
15 pub track_count: usize,
17}
18
19pub fn read(roots: &[String], excluded: &[String], id: &str) -> Result<Document, Error> {
21 read_with_open(roots, excluded, id, |path| fs::File::open(path))
22}
23
24fn read_with_open(
25 roots: &[String],
26 excluded: &[String],
27 id: &str,
28 open: impl FnOnce(&Path) -> std::io::Result<fs::File>,
29) -> Result<Document, Error> {
30 let suffix = id.strip_prefix("m3u-file-").ok_or(Error::InvalidIdentity)?;
31 if suffix.len() != 64
32 || !suffix
33 .bytes()
34 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
35 {
36 return Err(Error::InvalidIdentity);
37 }
38 let scope = || {
39 roots
40 .iter()
41 .chain(excluded)
42 .map(|path| Path::new(path).canonicalize().ok())
43 .collect::<Vec<_>>()
44 };
45 let before = scope();
46 let started = Instant::now();
47 let mut matches = discover(roots, excluded)?
48 .files
49 .into_iter()
50 .filter(|file| file.id == id);
51 let file = matches.next().ok_or(Error::NotFound)?;
52 if matches.next().is_some() {
53 return Err(Error::Changed);
54 }
55 let document = read_file(file, open)?;
56 if before != scope() {
57 return Err(Error::Changed);
58 }
59 if started.elapsed() >= LIMITS.elapsed {
60 return Err(Error::Timeout);
61 }
62 Ok(document)
63}
64
65fn safe_path(path: &Path) -> Result<fs::Metadata, Error> {
66 let metadata = fs::symlink_metadata(path).map_err(|_| Error::Unavailable)?;
67 if !metadata.is_file()
68 || metadata.file_type().is_symlink()
69 || path.canonicalize().map_err(|_| Error::Unavailable)? != path
70 {
71 return Err(Error::Changed);
72 }
73 if metadata.len() > MAX_BYTES {
74 return Err(Error::TooLarge);
75 }
76 Ok(metadata)
77}
78
79fn same_file(left: &fs::Metadata, right: &fs::Metadata) -> bool {
80 #[cfg(unix)]
81 {
82 use std::os::unix::fs::MetadataExt;
83 if left.dev() != right.dev() || left.ino() != right.ino() {
84 return false;
85 }
86 }
87 left.is_file()
88 && right.is_file()
89 && left.len() == right.len()
90 && left
91 .modified()
92 .ok()
93 .zip(right.modified().ok())
94 .is_some_and(|(left, right)| left == right)
95}
96
97fn read_file(
98 file: File,
99 open: impl FnOnce(&Path) -> std::io::Result<fs::File>,
100) -> Result<Document, Error> {
101 let path = Path::new(&file.path);
102 let before = safe_path(path)?;
103 let handle = open(path).map_err(|_| Error::Unavailable)?;
104 let opened = handle.metadata().map_err(|_| Error::Unavailable)?;
105 if !same_file(&before, &opened) || !same_file(&opened, &safe_path(path)?) {
106 return Err(Error::Changed);
107 }
108 let mut bytes = Vec::new();
109 (&handle)
110 .take(MAX_BYTES + 1)
111 .read_to_end(&mut bytes)
112 .map_err(|_| Error::Unavailable)?;
113 if bytes.len() as u64 > MAX_BYTES {
114 return Err(Error::TooLarge);
115 }
116 if !same_file(&opened, &handle.metadata().map_err(|_| Error::Unavailable)?)
117 || !same_file(&opened, &safe_path(path)?)
118 {
119 return Err(Error::Changed);
120 }
121 let content = String::from_utf8(bytes).map_err(|_| Error::InvalidText)?;
122 if content
123 .chars()
124 .any(|character| character.is_control() && !matches!(character, '\t' | '\r' | '\n'))
125 {
126 return Err(Error::InvalidText);
127 }
128 let track_count = crate::playlist::entry_lines(&content).count();
129 if track_count > MAX_TRACKS {
130 return Err(Error::TooLarge);
131 }
132 Ok(Document {
133 file,
134 content,
135 track_count,
136 })
137}
138
139#[cfg(test)]
140#[path = "read_tests.rs"]
141mod tests;