1use std::io::prelude::*;
15use std::{env, fs, io, path};
16
17pub const HEADER: &[u8; 43] = b"Signature: 8a477f597d28d172789f06886806bc55";
19
20pub fn is_tagged<P: AsRef<path::Path>>(directory: P) -> io::Result<bool> {
33 get_tag_state(directory).map(|state| matches!(state, TagState::Present))
34}
35
36pub fn get_tag_state<P: AsRef<path::Path>>(directory: P) -> io::Result<TagState> {
43 let directory = directory.as_ref();
44 match fs::File::open(directory.join("CACHEDIR.TAG")) {
45 Ok(mut cachedir_tag) => {
46 let mut buffer = vec![0; HEADER.len()];
47 let read = cachedir_tag.read(&mut buffer)?;
48 let header_ok = read == HEADER.len() && buffer == HEADER[..];
49 Ok(if header_ok {
50 TagState::Present
51 } else {
52 TagState::WrongHeader
53 })
54 }
55 Err(e) => match e.kind() {
56 io::ErrorKind::NotFound => {
57 if directory.is_dir() {
58 Ok(TagState::Absent)
59 } else {
60 Err(e)
61 }
62 }
63 _ => Err(e),
64 },
65 }
66}
67
68pub enum TagState {
70 Absent,
72 WrongHeader,
75 Present,
77}
78
79pub fn add_tag<P: AsRef<path::Path>>(directory: P) -> io::Result<()> {
87 let directory = directory.as_ref();
88 match fs::OpenOptions::new()
89 .write(true)
90 .create_new(true)
91 .open(directory.join("CACHEDIR.TAG"))
92 {
93 Ok(mut cachedir_tag) => cachedir_tag.write_all(HEADER),
94 Err(e) => Err(e),
95 }
96}
97
98pub fn ensure_tag<P: AsRef<path::Path>>(directory: P) -> io::Result<()> {
106 match add_tag(directory) {
107 Err(e) => match e.kind() {
108 io::ErrorKind::AlreadyExists => Ok(()),
109 _ => Err(e),
110 },
111 other => other,
112 }
113}
114
115pub fn mkdir_atomic<P: AsRef<path::Path>>(directory: P) -> io::Result<bool> {
129 let mut directory = directory.as_ref().to_path_buf();
130 if directory.exists() {
131 return Ok(false);
132 }
133
134 if directory.is_relative() {
135 directory = env::current_dir()?.join(directory);
136 }
137
138 let tempdir = tempfile::Builder::new()
139 .prefix(directory.file_name().unwrap())
140 .tempdir_in(directory.parent().unwrap())?;
141 add_tag(tempdir.path())?;
142 match fs::rename(tempdir.path(), &directory) {
143 Ok(()) => Ok(true),
144 Err(e) => {
145 if directory.is_dir() {
146 Ok(false)
147 } else {
148 Err(e)
149 }
150 }
151 }
152}
153
154#[test]
155fn is_tagged_on_nonexistent_directory_is_an_error() {
156 let directory = path::Path::new("this directory does not exist");
157 assert!(!directory.exists());
158 assert!(is_tagged(directory).is_err());
159}
160
161#[test]
162fn empty_directory_is_not_tagged() {
163 assert!(!is_tagged(tempfile::tempdir().unwrap()).unwrap());
164}
165
166#[test]
167fn directory_with_a_tag_with_wrong_content_is_not_tagged() {
168 let directory = tempfile::tempdir().unwrap();
169 let cachedir_tag = directory.path().join("CACHEDIR.TAG");
170
171 fs::write(&cachedir_tag, "").unwrap();
172 assert!(!is_tagged(&directory).unwrap());
173
174 fs::write(&cachedir_tag, &HEADER[..(HEADER.len() - 2)]).unwrap();
175 assert!(!is_tagged(&directory).unwrap());
176}
177
178#[test]
179fn add_tag_is_detected_by_is_tagged() {
180 let directory = tempfile::tempdir().unwrap();
181 add_tag(directory.path()).unwrap();
182 assert!(is_tagged(directory.path()).unwrap());
183}
184
185#[test]
186fn add_tag_errors_when_called_with_nonexistent_directory() {
187 let directory = path::Path::new("this directory does not exist");
188 assert!(!directory.exists());
189 assert!(add_tag(directory).is_err());
190}
191
192#[test]
193fn add_tag_errors_when_tag_already_exists() {
194 let directory = tempfile::tempdir().unwrap();
195 assert!(add_tag(directory.path()).is_ok());
196 assert!(add_tag(directory.path()).is_err());
197}
198
199#[test]
200fn ensure_tag_is_detected_by_is_tagged() {
201 let directory = tempfile::tempdir().unwrap();
202 ensure_tag(directory.path()).unwrap();
203 assert!(is_tagged(directory.path()).unwrap());
204}
205
206#[test]
207fn ensure_tag_errors_when_called_with_nonexistent_directory() {
208 let directory = path::Path::new("this directory does not exist");
209 assert!(!directory.exists());
210 assert!(ensure_tag(directory).is_err());
211 assert!(is_tagged(directory).is_err());
212}
213
214#[test]
215fn ensure_tag_is_idempotent() {
216 let directory = tempfile::tempdir().unwrap();
217 assert!(ensure_tag(directory.path()).is_ok());
218 assert!(is_tagged(directory.path()).unwrap());
219 assert!(ensure_tag(directory.path()).is_ok());
220 assert!(is_tagged(directory.path()).unwrap());
221}
222
223#[test]
224fn mkdir_atomic_works() {
225 use std::thread;
226 let directory = tempfile::tempdir().unwrap();
227 let cache = directory.path().join("cache");
228 let threads = (0..10).map(|_| {
229 let cache = cache.clone();
230 thread::spawn(move || mkdir_atomic(cache))
231 });
232 let results = threads.map(|t| t.join().unwrap().unwrap());
233 let creations: usize = results.map(|created| if created { 1 } else { 0 }).sum();
234 assert_eq!(creations, 1);
236 assert!(is_tagged(cache).unwrap());
238
239 assert_eq!(
242 fs::read_dir(directory.path())
243 .unwrap()
244 .map(|entry| entry.unwrap().file_name())
245 .collect::<Vec<_>>(),
246 ["cache"],
247 );
248}