1use std::io::{BufReader, Read};
12use std::path::Path;
13
14use crate::error::{Error, Result};
15
16pub type GzipReader<R> = flate2::read::MultiGzDecoder<R>;
20
21pub fn is_gzipped(first_two: &[u8]) -> bool {
23 first_two.len() >= 2 && first_two[0] == 0x1F && first_two[1] == 0x8B
24}
25
26pub fn open_text(path: impl AsRef<Path>) -> Result<(TextInput, u64)> {
37 let path = path.as_ref();
38 let display = path.to_string_lossy().into_owned();
39 let file = std::fs::File::open(path).map_err(|e| Error::io(&display, e))?;
40 let size = file.metadata().map_err(|e| Error::io(&display, e))?.len();
41
42 let mut head = [0u8; 2];
43 let read = read_full(&file, &mut head).map_err(|e| Error::io(&display, e))?;
44 use std::io::Seek as _;
48 let mut file = file;
49 file.rewind().map_err(|e| Error::io(&display, e))?;
50
51 let inner: Box<dyn Read + Send> = if is_gzipped(&head[..read]) {
52 Box::new(GzipReader::new(BufReader::with_capacity(1 << 16, file)))
53 } else {
54 Box::new(file)
55 };
56 Ok((BufReader::with_capacity(1 << 20, inner), size))
57}
58
59fn read_full(mut file: &std::fs::File, buf: &mut [u8]) -> std::io::Result<usize> {
60 let mut filled = 0;
61 while filled < buf.len() {
62 match file.read(&mut buf[filled..]) {
63 Ok(0) => break,
64 Ok(n) => filled += n,
65 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
66 Err(e) => return Err(e),
67 }
68 }
69 Ok(filled)
70}
71
72pub type TextInput = BufReader<Box<dyn Read + Send>>;
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use std::io::{BufRead as _, Write as _};
80
81 fn temp(name: &str) -> std::path::PathBuf {
82 let dir = std::env::temp_dir().join("gwseq_gzip_tests");
83 std::fs::create_dir_all(&dir).unwrap();
84 dir.join(name)
85 }
86
87 fn gzip_members(bodies: &[&[u8]]) -> Vec<u8> {
93 let mut out = Vec::new();
94 for body in bodies {
95 let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::new(6));
96 e.write_all(body).unwrap();
97 out.extend_from_slice(&e.finish().unwrap());
98 }
99 out
100 }
101
102 fn lines_of(path: &std::path::Path) -> Vec<String> {
103 let (reader, _) = open_text(path).unwrap();
104 reader.lines().map(|l| l.unwrap()).collect()
105 }
106
107 #[test]
108 fn plain_text_is_read_as_it_stands() {
109 let path = temp("plain.txt");
110 std::fs::write(&path, "one\ntwo\nthree\n").unwrap();
111 assert_eq!(lines_of(&path), ["one", "two", "three"]);
112 std::fs::remove_file(&path).ok();
113 }
114
115 #[test]
116 fn a_gzipped_file_is_inflated_whatever_it_is_called() {
117 let path = temp("nogz.bedgraph");
118 std::fs::write(&path, gzip_members(&[b"one\ntwo\nthree\n"])).unwrap();
119 assert_eq!(lines_of(&path), ["one", "two", "three"]);
120 std::fs::remove_file(&path).ok();
121 }
122
123 #[test]
124 fn every_member_of_a_multi_member_file_is_read() {
125 let path = temp("members.gz");
128 let bodies: Vec<Vec<u8>> = (0..50).map(|i| format!("line{i}\n").into_bytes()).collect();
129 let members = gzip_members(&bodies.iter().map(|b| &b[..]).collect::<Vec<_>>());
130 std::fs::write(&path, members).unwrap();
131 let lines = lines_of(&path);
132 assert_eq!(lines.len(), 50);
133 assert_eq!(lines[49], "line49");
134 std::fs::remove_file(&path).ok();
135 }
136
137 #[test]
138 fn the_reported_size_is_the_size_on_disk() {
139 let path = temp("size.txt");
140 std::fs::write(&path, vec![b'x'; 1234]).unwrap();
141 let (_, size) = open_text(&path).unwrap();
142 assert_eq!(size, 1234);
143 std::fs::remove_file(&path).ok();
144 }
145
146 #[test]
147 fn an_empty_file_reads_as_no_lines_rather_than_failing() {
148 let path = temp("empty.txt");
149 std::fs::write(&path, b"").unwrap();
150 assert!(lines_of(&path).is_empty());
151 std::fs::remove_file(&path).ok();
152 }
153}