1use std::fs::File;
13use std::path::Path;
14
15use bytes::Bytes;
16use parking_lot::RwLock;
17
18use crate::error::{Error, Result};
19use crate::source::ByteSource;
20
21#[derive(Debug)]
22pub struct LocalSource {
23 file: RwLock<Option<File>>,
27 path: String,
28 len: u64,
32}
33
34impl LocalSource {
35 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
36 let path = path.as_ref();
37 let display = path.to_string_lossy().into_owned();
38 let file = File::open(path).map_err(|e| Error::io(&display, e))?;
39 let len = file.metadata().map_err(|e| Error::io(&display, e))?.len();
40 Ok(Self {
41 file: RwLock::new(Some(file)),
42 path: display,
43 len,
44 })
45 }
46}
47
48impl ByteSource for LocalSource {
49 fn path(&self) -> &str {
50 &self.path
51 }
52
53 fn len(&self) -> Result<u64> {
54 Ok(self.len)
55 }
56
57 fn read_at(&self, offset: u64, len: usize) -> Result<Bytes> {
58 if len == 0 || offset >= self.len {
59 return Ok(Bytes::new());
60 }
61 let len = len.min((self.len - offset) as usize);
64
65 let guard = self.file.read();
66 let file = guard.as_ref().ok_or_else(|| Error::Closed {
67 path: self.path.clone(),
68 })?;
69
70 let mut buf = vec![0u8; len];
71 let read = pread(file, offset, &mut buf).map_err(|e| Error::io(&self.path, e))?;
72 buf.truncate(read);
73 Ok(Bytes::from(buf))
74 }
75
76 fn close(&self) {
77 *self.file.write() = None;
78 }
79}
80
81#[cfg(unix)]
87fn pread(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> {
88 use std::os::unix::fs::FileExt;
89 let mut filled = 0;
90 while filled < buf.len() {
91 match file.read_at(&mut buf[filled..], offset + filled as u64) {
92 Ok(0) => break,
93 Ok(n) => filled += n,
94 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
95 Err(e) => return Err(e),
96 }
97 }
98 Ok(filled)
99}
100
101#[cfg(windows)]
102fn pread(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> {
103 use std::os::windows::fs::FileExt;
104 let mut filled = 0;
105 while filled < buf.len() {
106 match file.seek_read(&mut buf[filled..], offset + filled as u64) {
107 Ok(0) => break,
108 Ok(n) => filled += n,
109 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
110 Err(e) => return Err(e),
111 }
112 }
113 Ok(filled)
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use std::io::Write;
120
121 fn fixture(bytes: &[u8]) -> (tempdir::TempPath, LocalSource) {
122 let path = tempdir::TempPath::new("local-source");
123 let mut f = File::create(&path.0).unwrap();
124 f.write_all(bytes).unwrap();
125 f.sync_all().unwrap();
126 let source = LocalSource::open(&path.0).unwrap();
127 (path, source)
128 }
129
130 pub mod tempdir {
133 pub struct TempPath(pub std::path::PathBuf);
134
135 impl TempPath {
136 pub fn new(tag: &str) -> Self {
137 let nanos = std::time::SystemTime::now()
138 .duration_since(std::time::UNIX_EPOCH)
139 .unwrap()
140 .as_nanos();
141 let mut p = std::env::temp_dir();
142 p.push(format!(
143 "gwseq-{tag}-{nanos}-{:?}",
144 std::thread::current().id()
145 ));
146 TempPath(p)
147 }
148 }
149
150 impl Drop for TempPath {
151 fn drop(&mut self) {
152 let _ = std::fs::remove_file(&self.0);
153 }
154 }
155 }
156
157 #[test]
158 fn reads_every_range_of_a_known_file() {
159 let data: Vec<u8> = (0..=255u8).collect();
160 let (_p, source) = fixture(&data);
161 assert_eq!(source.len().unwrap(), 256);
162 for offset in [0u64, 1, 127, 255] {
163 for len in [1usize, 2, 64, 300] {
164 let got = source.read_at(offset, len).unwrap();
165 let end = (offset as usize + len).min(data.len());
166 assert_eq!(&got[..], &data[offset as usize..end], "at {offset}+{len}");
167 }
168 }
169 }
170
171 #[test]
172 fn a_read_past_the_end_is_short_not_an_error() {
173 let (_p, source) = fixture(b"abcdef");
174 assert_eq!(&source.read_at(4, 100).unwrap()[..], b"ef");
175 assert!(source.read_at(6, 10).unwrap().is_empty());
176 assert!(source.read_at(99, 10).unwrap().is_empty());
177 assert!(source.read_at(0, 0).unwrap().is_empty());
178 }
179
180 #[test]
181 fn read_exact_at_refuses_a_short_read() {
182 let (_p, source) = fixture(b"abcdef");
183 assert_eq!(&source.read_exact_at(0, 6).unwrap()[..], b"abcdef");
184 let err = source.read_exact_at(4, 100).unwrap_err().to_string();
185 assert!(err.contains("wanted 100 bytes, got 2"), "{err}");
186 }
187
188 #[test]
189 fn read_to_end_takes_the_rest() {
190 let (_p, source) = fixture(b"abcdef");
191 assert_eq!(&source.read_to_end(2).unwrap()[..], b"cdef");
192 assert!(source.read_to_end(6).unwrap().is_empty());
193 }
194
195 #[test]
196 fn a_closed_source_refuses_and_names_the_file() {
197 let (path, source) = fixture(b"abcdef");
198 source.close();
199 source.close(); let err = source.read_at(0, 1).unwrap_err().to_string();
201 assert!(err.contains("is closed"), "{err}");
202 assert!(err.contains(path.0.to_str().unwrap()), "{err}");
205 }
206
207 #[test]
208 fn concurrent_readers_share_one_handle() {
209 let data: Vec<u8> = (0..64u8).cycle().take(1 << 16).collect();
210 let (_p, source) = fixture(&data);
211 let source = std::sync::Arc::new(source);
212 let threads: Vec<_> = (0..8)
213 .map(|t| {
214 let source = source.clone();
215 let data = data.clone();
216 std::thread::spawn(move || {
217 for i in 0..500u64 {
218 let offset = (t * 977 + i * 13) % 60_000;
219 let got = source.read_at(offset, 128).unwrap();
220 let end = (offset as usize + 128).min(data.len());
221 assert_eq!(&got[..], &data[offset as usize..end]);
222 }
223 })
224 })
225 .collect();
226 for t in threads {
227 t.join().unwrap();
228 }
229 }
230
231 #[test]
232 fn opening_a_missing_file_names_it() {
233 let err = LocalSource::open("/definitely/not/here.bigwig").unwrap_err();
234 assert!(matches!(err, Error::Io { .. }));
235 assert!(err.to_string().contains("here.bigwig"));
236 }
237}