1use std::io::{self, Read, Seek, SeekFrom};
46
47use oxideav_core::{BytesSource, Error, Result};
48
49use crate::data::open_data;
50use crate::file::open_file;
51use crate::mem::open_mem;
52use crate::slice::open_slice;
53use crate::uri;
54
55struct ConcatSource {
63 parts: Vec<Box<dyn BytesSource>>,
65 starts: Vec<u64>,
69 pos: u64,
71}
72
73impl ConcatSource {
74 fn new(mut parts: Vec<Box<dyn BytesSource>>) -> Result<Self> {
78 let mut starts = Vec::with_capacity(parts.len() + 1);
79 let mut acc: u64 = 0;
80 for part in parts.iter_mut() {
81 starts.push(acc);
82 let len = part.seek(SeekFrom::End(0))?;
83 acc = acc
84 .checked_add(len)
85 .ok_or_else(|| Error::invalid("concat: total length overflows u64"))?;
86 }
87 starts.push(acc);
88 if let Some(first) = parts.first_mut() {
91 first.seek(SeekFrom::Start(0))?;
92 }
93 Ok(Self {
94 parts,
95 starts,
96 pos: 0,
97 })
98 }
99
100 fn total_len(&self) -> u64 {
102 *self
103 .starts
104 .last()
105 .expect("starts always has a trailing total")
106 }
107
108 fn segment_for(&self, pos: u64) -> Option<usize> {
112 if pos >= self.total_len() {
113 return None;
114 }
115 (0..self.parts.len()).find(|&i| pos >= self.starts[i] && pos < self.starts[i + 1])
118 }
119}
120
121impl Read for ConcatSource {
122 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
123 if buf.is_empty() {
124 return Ok(0);
125 }
126 let idx = match self.segment_for(self.pos) {
127 Some(i) => i,
128 None => return Ok(0), };
130 let seg_start = self.starts[idx];
132 let seg_end = self.starts[idx + 1];
133 let intra = self.pos - seg_start;
134 let remaining_in_seg = (seg_end - self.pos) as usize;
135 let want = buf.len().min(remaining_in_seg);
136
137 let part = &mut self.parts[idx];
139 part.seek(SeekFrom::Start(intra))?;
140 let n = part.read(&mut buf[..want])?;
141 self.pos += n as u64;
142 Ok(n)
143 }
144}
145
146impl Seek for ConcatSource {
147 fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
148 let total = self.total_len();
149 let new_pos = match from {
150 SeekFrom::Start(off) => off,
151 SeekFrom::End(off) => add_signed(total, off)?,
152 SeekFrom::Current(off) => add_signed(self.pos, off)?,
153 };
154 self.pos = new_pos;
155 Ok(self.pos)
156 }
157}
158
159fn add_signed(base: u64, off: i64) -> io::Result<u64> {
163 let result = if off >= 0 {
164 base.checked_add(off as u64)
165 } else {
166 base.checked_sub(off.unsigned_abs())
167 };
168 result.ok_or_else(|| {
169 io::Error::new(
170 io::ErrorKind::InvalidInput,
171 "concat: seek resolves to a negative or overflowing position",
172 )
173 })
174}
175
176fn segments(rest: &str) -> Result<Vec<&str>> {
180 let parts: Vec<&str> = rest.split('|').collect();
181 if parts.iter().any(|s| s.is_empty()) {
182 return Err(Error::invalid(format!(
183 "concat: URI has an empty segment: {rest:?}"
184 )));
185 }
186 Ok(parts)
187}
188
189fn open_segment(seg: &str) -> Result<Box<dyn BytesSource>> {
195 let (seg_scheme, _) = uri::split(seg);
196 match seg_scheme {
197 "file" => open_file(seg),
198 "mem" => open_mem(seg),
199 "data" => open_data(seg),
200 "slice" => open_slice(seg),
201 "concat" => Err(Error::invalid(format!(
202 "concat: segment {seg:?} is itself a concat: URI; nesting concat is not supported \
203 because the outer '|' split would shred the inner segment list"
204 ))),
205 other => Err(Error::invalid(format!(
206 "concat: segment {seg:?} uses unsupported scheme {other:?}; \
207 only file/mem/data/slice are accepted"
208 ))),
209 }
210}
211
212pub fn open_concat(uri_str: &str) -> Result<Box<dyn BytesSource>> {
217 let (scheme, rest) = uri::split(uri_str);
218 if scheme != "concat" {
219 return Err(Error::invalid(format!(
220 "concat driver invoked on non-concat URI: {uri_str}"
221 )));
222 }
223 if rest.is_empty() {
224 return Err(Error::invalid("concat: URI requires at least one segment"));
225 }
226 let segs = segments(rest)?;
227 let mut parts: Vec<Box<dyn BytesSource>> = Vec::with_capacity(segs.len());
228 for seg in segs {
229 parts.push(open_segment(seg)?);
230 }
231 Ok(Box::new(ConcatSource::new(parts)?))
232}
233
234#[cfg(test)]
235mod tests {
236 use std::io::{Read, Seek, SeekFrom, Write};
237
238 use crate::mem;
239
240 use super::*;
241
242 fn temp_file(bytes: &[u8]) -> std::path::PathBuf {
244 use std::sync::atomic::{AtomicU64, Ordering};
245 static N: AtomicU64 = AtomicU64::new(0);
246 let mut path = std::env::temp_dir();
247 let pid = std::process::id();
248 let n = N.fetch_add(1, Ordering::Relaxed);
249 path.push(format!("oxideav-concat-test-{pid}-{n}.bin"));
250 let mut f = std::fs::File::create(&path).unwrap();
251 f.write_all(bytes).unwrap();
252 f.flush().unwrap();
253 path
254 }
255
256 fn uri_for(paths: &[&std::path::Path]) -> String {
257 let joined: Vec<String> = paths.iter().map(|p| p.display().to_string()).collect();
258 format!("concat:{}", joined.join("|"))
259 }
260
261 #[test]
262 fn two_files_read_back_to_back() {
263 let a = temp_file(b"Hello, ");
264 let b = temp_file(b"world!");
265 let mut r = open_concat(&uri_for(&[&a, &b])).unwrap();
266 let mut buf = Vec::new();
267 r.read_to_end(&mut buf).unwrap();
268 assert_eq!(buf, b"Hello, world!");
269 std::fs::remove_file(a).ok();
270 std::fs::remove_file(b).ok();
271 }
272
273 #[test]
274 fn three_segments() {
275 let a = temp_file(b"AAA");
276 let b = temp_file(b"BB");
277 let c = temp_file(b"CCCC");
278 let mut r = open_concat(&uri_for(&[&a, &b, &c])).unwrap();
279 let mut buf = Vec::new();
280 r.read_to_end(&mut buf).unwrap();
281 assert_eq!(buf, b"AAABBCCCC");
282 for p in [a, b, c] {
283 std::fs::remove_file(p).ok();
284 }
285 }
286
287 #[test]
288 fn small_buffer_reads_cross_boundary() {
289 let a = temp_file(b"XY");
291 let b = temp_file(b"Z");
292 let mut r = open_concat(&uri_for(&[&a, &b])).unwrap();
293 let mut out = Vec::new();
294 let mut byte = [0u8; 1];
295 loop {
296 let n = r.read(&mut byte).unwrap();
297 if n == 0 {
298 break;
299 }
300 out.push(byte[0]);
301 }
302 assert_eq!(out, b"XYZ");
303 std::fs::remove_file(a).ok();
304 std::fs::remove_file(b).ok();
305 }
306
307 #[test]
308 fn seek_end_reports_total_length() {
309 let a = temp_file(b"12345");
310 let b = temp_file(b"678");
311 let mut r = open_concat(&uri_for(&[&a, &b])).unwrap();
312 let end = r.seek(SeekFrom::End(0)).unwrap();
313 assert_eq!(end, 8);
314 std::fs::remove_file(a).ok();
315 std::fs::remove_file(b).ok();
316 }
317
318 #[test]
319 fn seek_into_second_segment() {
320 let a = temp_file(b"abcd"); let b = temp_file(b"EFGH"); let mut r = open_concat(&uri_for(&[&a, &b])).unwrap();
323 r.seek(SeekFrom::Start(5)).unwrap(); let mut byte = [0u8; 1];
325 r.read_exact(&mut byte).unwrap();
326 assert_eq!(byte[0], b'F');
327 std::fs::remove_file(a).ok();
328 std::fs::remove_file(b).ok();
329 }
330
331 #[test]
332 fn seek_across_boundary_then_read() {
333 let a = temp_file(b"abc"); let b = temp_file(b"defg"); let mut r = open_concat(&uri_for(&[&a, &b])).unwrap();
336 r.seek(SeekFrom::Start(2)).unwrap();
337 let mut buf = [0u8; 4]; r.read_exact(&mut buf).unwrap();
339 assert_eq!(&buf, b"cdef");
340 std::fs::remove_file(a).ok();
341 std::fs::remove_file(b).ok();
342 }
343
344 #[test]
345 fn seek_current_relative() {
346 let a = temp_file(b"0123456789");
347 let mut r = open_concat(&uri_for(&[&a])).unwrap();
348 r.seek(SeekFrom::Start(3)).unwrap();
349 let p = r.seek(SeekFrom::Current(2)).unwrap();
350 assert_eq!(p, 5);
351 let mut byte = [0u8; 1];
352 r.read_exact(&mut byte).unwrap();
353 assert_eq!(byte[0], b'5');
354 std::fs::remove_file(a).ok();
355 }
356
357 #[test]
358 fn seek_before_zero_errors() {
359 let a = temp_file(b"abc");
360 let mut r = open_concat(&uri_for(&[&a])).unwrap();
361 let res = r.seek(SeekFrom::Current(-1));
362 assert!(res.is_err());
363 std::fs::remove_file(a).ok();
364 }
365
366 #[test]
367 fn read_at_eof_returns_zero() {
368 let a = temp_file(b"hi");
369 let mut r = open_concat(&uri_for(&[&a])).unwrap();
370 r.seek(SeekFrom::End(0)).unwrap();
371 let mut buf = [0u8; 4];
372 assert_eq!(r.read(&mut buf).unwrap(), 0);
373 std::fs::remove_file(a).ok();
374 }
375
376 #[test]
377 fn empty_segment_rejected() {
378 assert!(open_concat("concat:a||b").is_err());
379 assert!(open_concat("concat:a|").is_err());
380 assert!(open_concat("concat:|a").is_err());
381 }
382
383 #[test]
384 fn no_segments_rejected() {
385 assert!(open_concat("concat:").is_err());
386 }
387
388 #[test]
389 fn wrong_scheme_rejected() {
390 assert!(open_concat("file:///tmp/x").is_err());
391 assert!(open_concat("mem://x").is_err());
392 }
393
394 #[test]
395 fn file_url_segment_accepted() {
396 let a = temp_file(b"pre-");
397 let b = temp_file(b"post");
398 let uri = format!("concat:file://{}|file://{}", a.display(), b.display());
399 let mut r = open_concat(&uri).unwrap();
400 let mut buf = Vec::new();
401 r.read_to_end(&mut buf).unwrap();
402 assert_eq!(buf, b"pre-post");
403 std::fs::remove_file(a).ok();
404 std::fs::remove_file(b).ok();
405 }
406
407 #[test]
408 fn missing_file_segment_errors() {
409 let a = temp_file(b"ok");
410 let uri = format!("concat:{}|/no/such/path/xyzzy-oxideav", a.display());
411 assert!(open_concat(&uri).is_err());
412 std::fs::remove_file(a).ok();
413 }
414
415 #[test]
416 fn empty_file_segment_is_transparent() {
417 let a = temp_file(b"AB");
419 let empty = temp_file(b"");
420 let c = temp_file(b"CD");
421 let mut r = open_concat(&uri_for(&[&a, &empty, &c])).unwrap();
422 let mut buf = Vec::new();
423 r.read_to_end(&mut buf).unwrap();
424 assert_eq!(buf, b"ABCD");
425 for p in [a, empty, c] {
426 std::fs::remove_file(p).ok();
427 }
428 }
429
430 #[test]
431 fn data_uri_segment_accepted() {
432 let mut r = open_concat("concat:data:,Hello%2C%20|data:,world%21").unwrap();
434 let mut buf = Vec::new();
435 r.read_to_end(&mut buf).unwrap();
436 assert_eq!(buf, b"Hello, world!");
437 }
438
439 #[test]
440 fn mem_segment_accepted() {
441 mem::put("concat-r184-a", b"AAA".to_vec());
442 mem::put("concat-r184-b", b"BB".to_vec());
443 let mut r = open_concat("concat:mem://concat-r184-a|mem://concat-r184-b").unwrap();
444 let mut buf = Vec::new();
445 r.read_to_end(&mut buf).unwrap();
446 assert_eq!(buf, b"AAABB");
447 mem::remove("concat-r184-a");
448 mem::remove("concat-r184-b");
449 }
450
451 #[test]
452 fn slice_segment_accepted() {
453 mem::put("concat-r184-slc", b"abcdefgh".to_vec());
455 let f = temp_file(b"XYZ");
456 let uri = format!("concat:slice:2+3!mem://concat-r184-slc|{}", f.display());
457 let mut r = open_concat(&uri).unwrap();
458 let mut buf = Vec::new();
459 r.read_to_end(&mut buf).unwrap();
460 assert_eq!(buf, b"cdeXYZ");
461 mem::remove("concat-r184-slc");
462 std::fs::remove_file(f).ok();
463 }
464
465 #[test]
466 fn mixed_schemes_concat_in_order() {
467 mem::put("concat-r184-mix", b"MID".to_vec());
470 let head = temp_file(b"HEAD"); let uri = format!("concat:{}|mem://concat-r184-mix|data:,TAIL", head.display());
474 let mut r = open_concat(&uri).unwrap();
475 let total = r.seek(SeekFrom::End(0)).unwrap();
476 assert_eq!(total, 11);
477 r.seek(SeekFrom::Start(3)).unwrap(); let mut buf = vec![0u8; 6]; r.read_exact(&mut buf).unwrap();
480 assert_eq!(&buf, b"DMIDTA");
481 mem::remove("concat-r184-mix");
482 std::fs::remove_file(head).ok();
483 }
484
485 #[test]
486 fn nested_concat_segment_rejected() {
487 let res = open_concat("concat:concat:a|b|c");
491 let err = res.err().expect("nested concat: must be rejected");
492 let msg = err.to_string();
493 assert!(
494 msg.contains("nesting concat") || msg.contains("not supported"),
495 "expected nesting-concat rejection, got {msg}"
496 );
497 }
498
499 #[test]
500 fn unsupported_inner_scheme_rejected() {
501 let res = open_concat("concat:http://example.com/a|http://example.com/b");
504 assert!(res.is_err());
505 }
506}