1#![deny(missing_docs)]
43
44use sha2::{Digest, Sha256};
45use test_file::{TestFile, Compression};
46use std::{
47 borrow::Cow,
48 env::{self, VarError},
49 fs, io,
50 path::{Path, PathBuf},
51};
52
53mod entries;
54
55pub(crate) mod test_file;
56
57
58use entries::FILE_ENTRIES;
59
60#[derive(Debug)]
62pub enum Error {
63 NotFound,
67 InvalidHash,
71 Download(String),
73 Io(io::Error),
75 ResolveUrl(VarError),
77 ZstdRequired,
79}
80
81impl From<io::Error> for Error {
82 fn from(err: io::Error) -> Error {
83 Error::Io(err)
84 }
85}
86
87type Result<T, E = Error> = std::result::Result<T, E>;
88
89fn lookup(name: &str) -> Option<&'static TestFile> {
90 FILE_ENTRIES.iter().find(|entry| entry.name == name)
91}
92
93pub fn path(name: &str) -> Result<PathBuf, Error> {
100 let entry = lookup(name).ok_or(Error::NotFound)?;
101 let cached_path = get_data_path().join(entry.name);
102 if !cached_path.exists() {
103 download(name, &cached_path)?;
104 }
105 Ok(cached_path)
106}
107
108#[deprecated(note = "Too expensive. Use `path` for the files that you need.")]
116pub fn all() -> Result<Vec<PathBuf>, Error> {
117 FILE_ENTRIES
118 .iter()
119 .map(|TestFile { name, ..}| path(name))
120 .collect::<Result<Vec<PathBuf>, Error>>()
121}
122
123pub(crate) fn get_data_path() -> PathBuf {
125 let mut target_dir = PathBuf::from(
126 env::current_exe()
127 .expect("exe path")
128 .parent()
129 .expect("exe parent"),
130 );
131 while target_dir.file_name() != Some(std::ffi::OsStr::new("target")) {
132 if !target_dir.pop() {
133 panic!("Cannot find target directory");
134 }
135 }
136 target_dir.join("dicom_test_files")
137}
138
139const DEFAULT_GITHUB_BASE_URL: &str =
140 "https://raw.githubusercontent.com/robyoung/dicom-test-files/master/data/";
141
142const RAW_GITHUBUSERCONTENT_URL: &str = "https://raw.githubusercontent.com";
143
144fn base_url() -> Result<Cow<'static, str>, VarError> {
149 if let Ok(url) = std::env::var("DICOM_TEST_FILES_URL") {
150 if url != "" {
151 let url = if !url.ends_with("/") {
152 format!("{url}/")
153 } else {
154 url
155 };
156 return Ok(url.into());
157 }
158 }
159
160 let ci = std::env::var("CI").unwrap_or_default();
162 if ci == "true" {
163 let github_repository = std::env::var("GITHUB_REPOSITORY").unwrap_or_default();
165
166 if github_repository.ends_with("/dicom-test-files") {
168 let github_event_name = std::env::var("GITHUB_EVENT_NAME")?;
170 if github_event_name == "pull_request" {
171 let github_head_ref = std::env::var("GITHUB_HEAD_REF")?;
173 let url = format!(
174 "{}/{}/{}/data/",
175 RAW_GITHUBUSERCONTENT_URL, github_repository, github_head_ref
176 );
177
178 return Ok(url.into());
179 }
180 }
181 }
182
183 Ok(DEFAULT_GITHUB_BASE_URL.into())
184}
185
186fn download(name: &str, cached_path: &PathBuf) -> Result<(), Error> {
187 let file_entry = lookup(name).ok_or(Error::NotFound)?;
188
189 let target_parent_dir = cached_path.as_path().parent().unwrap();
190 fs::create_dir_all(target_parent_dir)?;
191
192 let url = base_url().map_err(Error::ResolveUrl)?.to_owned() + file_entry.real_file_name();
193 let resp = ureq::get(url.as_ref())
194 .call()
195 .map_err(|e| Error::Download(format!("Failed to download {}: {}", url, e)))?;
196
197 let tempdir = tempfile::tempdir_in(target_parent_dir)?;
199 let tempfile_path = tempdir.path().join("tmpfile");
200
201 {
202 let mut target = fs::File::create(&tempfile_path)?;
203 std::io::copy(&mut resp.into_body().as_reader(), &mut target)?;
204 }
205
206 check_hash(&tempfile_path, file_entry)?;
207 match file_entry.compression {
208 Compression::None => {
209 fs::rename(tempfile_path, cached_path.as_path())?;
211 },
212 Compression::Zstd => {
213 write_zstd(tempfile_path.as_path(), cached_path.as_path())?;
215
216 fs::remove_file(tempfile_path).unwrap_or_else(|e| {
218 eprintln!("[dicom-test-files] Failed to remove temporary file: {}", e);
219 });
220 }
221 }
222
223 Ok(())
224}
225
226#[cfg(feature = "zstd")]
227fn write_zstd(source_path: impl AsRef<Path>, cached_path: impl AsRef<Path>) -> Result<()> {
228 let mut decoder = zstd::Decoder::new(fs::File::open(source_path)?)?;
229 let mut target = fs::File::create(cached_path)?;
230 std::io::copy(&mut decoder, &mut target)?;
231 Ok(())
232}
233
234#[cfg(not(feature = "zstd"))]
235fn write_zstd(_source_path: impl AsRef<Path>, _cached_path: impl AsRef<Path>) -> Result<()> {
236 Err(Error::ZstdRequired)
237}
238
239fn check_hash(path: impl AsRef<Path>, file_entry: &TestFile) -> Result<()> {
240 let mut file = fs::File::open(path.as_ref())?;
241 let mut hasher = Sha256::new();
242 io::copy(&mut file, &mut hasher)?;
243 let hash = hasher.finalize();
244
245 if format!("{:x}", hash) != file_entry.hash {
246 fs::remove_file(path)?;
247 return Err(Error::InvalidHash);
248 }
249
250 Ok(())
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn load_a_single_path_1() {
259 let cached_path = get_data_path().join("pydicom/liver.dcm");
261 let _ = fs::remove_file(cached_path);
262
263 let path = path("pydicom/liver.dcm").unwrap();
264 let path = path.as_path();
265
266 assert_eq!(path.file_name().unwrap(), "liver.dcm");
267 assert!(path.exists());
268 }
269
270 #[test]
271 fn load_a_single_path_wg04_1() {
272 const FILE: &str = "WG04/JPLY/NM1_JPLY";
273 let cached_path = get_data_path().join(FILE);
275 let _ = fs::remove_file(cached_path);
276
277 let path = path(FILE).unwrap();
278 let path = path.as_path();
279
280 assert_eq!(path.file_name().unwrap(), "NM1_JPLY");
281 assert!(path.exists());
282
283 let metadata = std::fs::metadata(path).unwrap();
284 assert_eq!(metadata.len(), 9844);
286 }
287
288 #[cfg(feature = "zstd")]
289 #[test]
290 fn load_path_wg04_unc_1() {
291 const FILE: &str = "WG04/REF/NM1_UNC";
292 let cached_path = get_data_path().join(FILE);
294 let _ = fs::remove_file(cached_path);
295
296 let path = path(FILE).unwrap();
297 let path = path.as_path();
298
299 assert_eq!(path.file_name().unwrap(), "NM1_UNC");
300 assert!(path.exists());
301
302 let metadata = std::fs::metadata(path).unwrap();
303 assert_eq!(metadata.len(), 527066);
305 }
306
307 fn load_a_single_path_2() {
308 let cached_path = get_data_path().join("pydicom/CT_small.dcm");
310 let _ = fs::remove_file(cached_path);
311
312 let path = path("pydicom/CT_small.dcm").unwrap();
313 let path = path.as_path();
314
315 assert_eq!(path.file_name().unwrap(), "CT_small.dcm");
316 assert!(path.exists());
317 }
318
319 #[test]
320 fn load_a_single_path_concurrent() {
321 let handles: Vec<_> = (0..4)
322 .map(|_| std::thread::spawn(load_a_single_path_2))
323 .collect();
324 for h in handles {
325 h.join().unwrap();
326 }
327 }
328}