1use crate::Point;
6use std::{rc::Rc, sync::Arc};
7
8pub const BUNDLE_MIME: &str = "application/vnd.day.transfer";
9pub const MAX_BYTES: usize = 64 * 1024 * 1024;
10pub const MAX_ITEMS: usize = 256;
11
12#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Representation {
15 pub mime: String,
16 pub bytes: Arc<Vec<u8>>,
17}
18impl Representation {
19 pub fn new(mime: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
20 Self {
21 mime: mime.into(),
22 bytes: Arc::new(bytes.into()),
23 }
24 }
25}
26#[derive(Clone, Debug, Default, PartialEq, Eq)]
28pub struct Item {
29 pub representations: Vec<Representation>,
30}
31impl Item {
32 pub fn new(representations: Vec<Representation>) -> Self {
33 Self { representations }
34 }
35 pub fn get(&self, mime: &str) -> Option<&[u8]> {
36 self.representations
37 .iter()
38 .find(|r| r.mime == mime)
39 .map(|r| r.bytes.as_slice())
40 }
41}
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
43pub enum Operation {
44 #[default]
45 None,
46 Copy,
47 Move,
48 Link,
49}
50#[derive(Clone, Debug, Default, PartialEq, Eq)]
51pub struct Offer {
52 pub items: Vec<Item>,
53}
54
55#[derive(Clone, Debug)]
56pub struct Location {
57 pub position: Point,
59 pub types: Vec<String>,
60 pub allowed: Vec<Operation>,
61 pub local: bool,
63}
64impl Location {
65 pub fn has(&self, mime: &str) -> bool {
66 self.types.iter().any(|m| m == mime)
67 }
68}
69#[derive(Clone, Debug)]
70pub struct Drop {
71 pub local: bool,
72 pub position: Point,
73 pub operation: Operation,
74 pub items: Vec<Item>,
75}
76pub type Source = Rc<dyn Fn(Point) -> Option<Offer>>;
78#[derive(Clone)]
82pub struct Target {
83 pub types: Vec<String>,
84 pub accept: Rc<dyn Fn(&Location) -> Operation>,
85 pub receive: Rc<dyn Fn(Drop) -> bool>,
86}
87impl Target {
88 pub fn proposal(&self, location: &Location) -> Operation {
89 let op = crate::ffi_guard::contain(Operation::None, || (self.accept)(location));
90 if location.allowed.contains(&op) {
91 op
92 } else {
93 Operation::None
94 }
95 }
96 pub fn deliver(&self, location: Location, offer: Offer) -> bool {
97 let op = self.proposal(&location);
98 op != Operation::None
99 && crate::ffi_guard::contain(false, || {
100 (self.receive)(Drop {
101 local: location.local,
102 position: location.position,
103 operation: op,
104 items: offer.items,
105 })
106 })
107 }
108}
109
110impl Offer {
113 pub fn types(&self) -> Vec<String> {
114 let mut out = Vec::new();
115 for r in self.items.iter().flat_map(|i| &i.representations) {
116 if !out.contains(&r.mime) {
117 out.push(r.mime.clone());
118 }
119 }
120 out
121 }
122 pub fn encode(&self) -> Option<Vec<u8>> {
123 if self.items.is_empty() || self.items.len() > MAX_ITEMS {
124 return None;
125 }
126 let mut out = b"DAYDND\0\x01".to_vec();
127 out.extend_from_slice(&(self.items.len() as u32).to_le_bytes());
128 for item in &self.items {
129 if item.representations.is_empty() || item.representations.len() > 32 {
130 return None;
131 }
132 out.extend_from_slice(&(item.representations.len() as u32).to_le_bytes());
133 let mut seen = std::collections::HashSet::new();
134 for r in &item.representations {
135 if !valid_mime(&r.mime) || r.mime == BUNDLE_MIME || !seen.insert(&r.mime) {
136 return None;
137 }
138 if out
139 .len()
140 .checked_add(r.mime.len() + 8)?
141 .checked_add(r.bytes.len())?
142 > MAX_BYTES
143 {
144 return None;
145 }
146 out.extend_from_slice(&(r.mime.len() as u32).to_le_bytes());
147 out.extend_from_slice(&(r.bytes.len() as u32).to_le_bytes());
148 out.extend_from_slice(r.mime.as_bytes());
149 out.extend_from_slice(&r.bytes);
150 }
151 }
152 Some(out)
153 }
154 pub fn decode(mut bytes: &[u8]) -> Option<Self> {
155 fn take<'a>(b: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
156 let (a, rest) = b.split_at_checked(n)?;
157 *b = rest;
158 Some(a)
159 }
160 fn number(b: &mut &[u8]) -> Option<usize> {
161 Some(u32::from_le_bytes(take(b, 4)?.try_into().ok()?) as usize)
162 }
163 if bytes.len() > MAX_BYTES || take(&mut bytes, 8)? != b"DAYDND\0\x01" {
164 return None;
165 }
166 let count = number(&mut bytes)?;
167 if count == 0 || count > MAX_ITEMS {
168 return None;
169 }
170 let mut items = Vec::new();
171 for _ in 0..count {
172 let count = number(&mut bytes)?;
173 if count == 0 || count > 32 {
174 return None;
175 }
176 let mut representations = Vec::new();
177 for _ in 0..count {
178 let m = number(&mut bytes)?;
179 let n = number(&mut bytes)?;
180 if m > 255 {
181 return None;
182 }
183 let mime = std::str::from_utf8(take(&mut bytes, m)?).ok()?;
184 if !valid_mime(mime)
185 || mime == BUNDLE_MIME
186 || representations
187 .iter()
188 .any(|r: &Representation| r.mime == mime)
189 {
190 return None;
191 }
192 representations.push(Representation::new(mime, take(&mut bytes, n)?));
193 }
194 items.push(Item { representations });
195 }
196 if !bytes.is_empty() {
197 return None;
198 }
199 Some(Self { items })
200 }
201}
202pub fn valid_mime(m: &str) -> bool {
203 m.len() <= 255
204 && m.split_once('/')
205 .is_some_and(|(a, b)| !a.is_empty() && !b.is_empty() && !b.contains('/'))
206 && m.bytes()
207 .all(|c| c.is_ascii_alphanumeric() || b"/!#$&^_.+-".contains(&c))
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 #[test]
214 fn items_and_alternatives_survive_untrusted_transport() {
215 let offer = Offer {
216 items: vec![
217 Item::new(vec![
218 Representation::new("image/png", [0, 255]),
219 Representation::new("application/x-test", [9, 0]),
220 ]),
221 Item::new(vec![Representation::new(
222 "text/uri-list",
223 b"file:///tmp/a%20b\r\n",
224 )]),
225 ],
226 };
227 let encoded = offer.encode().unwrap();
228 assert_eq!(Offer::decode(&encoded), Some(offer));
229 for n in 0..encoded.len() {
230 assert!(Offer::decode(&encoded[..n]).is_none());
231 }
232 let mut extra = encoded;
233 extra.push(0);
234 assert!(Offer::decode(&extra).is_none());
235 }
236 #[test]
237 fn location_guard_is_rechecked_and_cannot_escalate_operations() {
238 let target = Target {
239 types: vec![],
240 accept: Rc::new(|p| {
241 if p.position.x < 50.0 {
242 Operation::Copy
243 } else {
244 Operation::Move
245 }
246 }),
247 receive: Rc::new(|_| true),
248 };
249 let mut p = Location {
250 position: Point::new(5., 5.),
251 types: vec![],
252 allowed: vec![Operation::Copy],
253 local: true,
254 };
255 assert_eq!(target.proposal(&p), Operation::Copy);
256 p.position.x = 80.;
257 assert!(!target.deliver(p, Offer::default()));
258 }
259}
260
261pub fn file_paths(bytes: &[u8]) -> Option<Vec<std::path::PathBuf>> {
264 let text = std::str::from_utf8(bytes).ok()?;
265 let mut paths = Vec::new();
266 for line in text
267 .lines()
268 .map(str::trim)
269 .filter(|l| !l.is_empty() && !l.starts_with('#'))
270 {
271 let uri = line.strip_prefix("file://")?;
272 let path = if uri.starts_with('/') {
273 uri
274 } else {
275 uri.strip_prefix("localhost/")
276 .map(|p| &uri[uri.len() - p.len() - 1..])?
277 };
278 if path.contains(['?', '#']) {
279 return None;
280 }
281 let mut decoded = Vec::with_capacity(path.len());
282 let mut raw = path.bytes();
283 while let Some(b) = raw.next() {
284 let b = if b == b'%' {
285 let hi = (raw.next()? as char).to_digit(16)?;
286 let lo = (raw.next()? as char).to_digit(16)?;
287 (hi * 16 + lo) as u8
288 } else {
289 b
290 };
291 if b == 0 {
292 return None;
293 }
294 decoded.push(b);
295 }
296 #[cfg(unix)]
297 let path = {
298 use std::os::unix::ffi::OsStringExt;
299 std::path::PathBuf::from(std::ffi::OsString::from_vec(decoded))
300 };
301 #[cfg(not(unix))]
302 let path = {
303 let s = String::from_utf8(decoded).ok()?;
304 #[cfg(windows)]
305 let s = if s.as_bytes().get(2) == Some(&b':') {
306 s[1..].to_string()
307 } else {
308 s
309 };
310 std::path::PathBuf::from(s)
311 };
312 paths.push(path);
313 if paths.len() > MAX_ITEMS {
314 return None;
315 }
316 }
317 (!paths.is_empty()).then_some(paths)
318}
319
320#[cfg(test)]
321mod file_tests {
322 use super::*;
323 #[test]
324 fn uri_references_decode_without_fetching_remote_resources() {
325 assert_eq!(
326 file_paths(b"# files\r\nfile:///tmp/a%20b%23c.png\r\nfile://localhost/tmp/two\r\n")
327 .unwrap(),
328 vec![
329 std::path::PathBuf::from("/tmp/a b#c.png"),
330 std::path::PathBuf::from("/tmp/two")
331 ]
332 );
333 for s in [
334 "https://example.com/a",
335 "file://remote/a",
336 "file:///tmp/%00",
337 "file:///tmp/%xx",
338 "file:///tmp/a#fragment",
339 ] {
340 assert!(file_paths(s.as_bytes()).is_none());
341 }
342 }
343}