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