• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*!
2 This crate provides a safe and simple **cross platform** way to determine
3 whether two file paths refer to the same file or directory.
4 
5 Most uses of this crate should be limited to the top-level [`is_same_file`]
6 function, which takes two file paths and returns true if they refer to the
7 same file or directory:
8 
9 ```rust,no_run
10 # use std::error::Error;
11 use same_file::is_same_file;
12 
13 # fn try_main() -> Result<(), Box<Error>> {
14 assert!(is_same_file("/bin/sh", "/usr/bin/sh")?);
15 #    Ok(())
16 # }
17 #
18 # fn main() {
19 #    try_main().unwrap();
20 # }
21 ```
22 
23 Additionally, this crate provides a [`Handle`] type that permits a more efficient
24 equality check depending on your access pattern. For example, if one wanted to
25 check whether any path in a list of paths corresponded to the process' stdout
26 handle, then one could build a handle once for stdout. The equality check for
27 each file in the list then only requires one stat call instead of two. The code
28 might look like this:
29 
30 ```rust,no_run
31 # use std::error::Error;
32 use same_file::Handle;
33 
34 # fn try_main() -> Result<(), Box<Error>> {
35 let candidates = &[
36     "examples/is_same_file.rs",
37     "examples/is_stderr.rs",
38     "examples/stderr",
39 ];
40 let stdout_handle = Handle::stdout()?;
41 for candidate in candidates {
42     let handle = Handle::from_path(candidate)?;
43     if stdout_handle == handle {
44         println!("{:?} is stdout!", candidate);
45     } else {
46         println!("{:?} is NOT stdout!", candidate);
47     }
48 }
49 #    Ok(())
50 # }
51 #
52 # fn main() {
53 #     try_main().unwrap();
54 # }
55 ```
56 
57 See [`examples/is_stderr.rs`] for a runnable example and compare the output of:
58 
59 - `cargo run --example is_stderr 2> examples/stderr` and
60 - `cargo run --example is_stderr`.
61 
62 [`is_same_file`]: fn.is_same_file.html
63 [`Handle`]: struct.Handle.html
64 [`examples/is_stderr.rs`]: https://github.com/BurntSushi/same-file/blob/master/examples/is_same_file.rs
65 
66 */
67 
68 #![allow(bare_trait_objects, unknown_lints)]
69 #![deny(missing_docs)]
70 
71 #[cfg(test)]
72 doc_comment::doctest!("../README.md");
73 
74 use std::fs::File;
75 use std::io;
76 use std::path::Path;
77 
78 #[cfg(any(target_os = "redox", unix))]
79 use crate::unix as imp;
80 #[cfg(not(any(target_os = "redox", unix, windows)))]
81 use unknown as imp;
82 #[cfg(windows)]
83 use win as imp;
84 
85 #[cfg(any(target_os = "redox", unix))]
86 mod unix;
87 #[cfg(not(any(target_os = "redox", unix, windows)))]
88 mod unknown;
89 #[cfg(windows)]
90 mod win;
91 
92 /// A handle to a file that can be tested for equality with other handles.
93 ///
94 /// If two files are the same, then any two handles of those files will compare
95 /// equal. If two files are not the same, then any two handles of those files
96 /// will compare not-equal.
97 ///
98 /// A handle consumes an open file resource as long as it exists.
99 ///
100 /// Equality is determined by comparing inode numbers on Unix and a combination
101 /// of identifier, volume serial, and file size on Windows. Note that it's
102 /// possible for comparing two handles to produce a false positive on some
103 /// platforms. Namely, two handles can compare equal even if the two handles
104 /// *don't* point to the same file. Check the [source] for specific
105 /// implementation details.
106 ///
107 /// [source]: https://github.com/BurntSushi/same-file/tree/master/src
108 #[derive(Debug, Eq, PartialEq, Hash)]
109 pub struct Handle(imp::Handle);
110 
111 impl Handle {
112     /// Construct a handle from a path.
113     ///
114     /// Note that the underlying [`File`] is opened in read-only mode on all
115     /// platforms.
116     ///
117     /// [`File`]: https://doc.rust-lang.org/std/fs/struct.File.html
118     ///
119     /// # Errors
120     /// This method will return an [`io::Error`] if the path cannot
121     /// be opened, or the file's metadata cannot be obtained.
122     /// The most common reasons for this are: the path does not
123     /// exist, or there were not enough permissions.
124     ///
125     /// [`io::Error`]: https://doc.rust-lang.org/std/io/struct.Error.html
126     ///
127     /// # Examples
128     /// Check that two paths are not the same file:
129     ///
130     /// ```rust,no_run
131     /// # use std::error::Error;
132     /// use same_file::Handle;
133     ///
134     /// # fn try_main() -> Result<(), Box<Error>> {
135     /// let source = Handle::from_path("./source")?;
136     /// let target = Handle::from_path("./target")?;
137     /// assert_ne!(source, target, "The files are the same.");
138     /// # Ok(())
139     /// # }
140     /// #
141     /// # fn main() {
142     /// #     try_main().unwrap();
143     /// # }
144     /// ```
from_path<P: AsRef<Path>>(p: P) -> io::Result<Handle>145     pub fn from_path<P: AsRef<Path>>(p: P) -> io::Result<Handle> {
146         imp::Handle::from_path(p).map(Handle)
147     }
148 
149     /// Construct a handle from a file.
150     ///
151     /// # Errors
152     /// This method will return an [`io::Error`] if the metadata for
153     /// the given [`File`] cannot be obtained.
154     ///
155     /// [`io::Error`]: https://doc.rust-lang.org/std/io/struct.Error.html
156     /// [`File`]: https://doc.rust-lang.org/std/fs/struct.File.html
157     ///
158     /// # Examples
159     /// Check that two files are not in fact the same file:
160     ///
161     /// ```rust,no_run
162     /// # use std::error::Error;
163     /// # use std::fs::File;
164     /// use same_file::Handle;
165     ///
166     /// # fn try_main() -> Result<(), Box<Error>> {
167     /// let source = File::open("./source")?;
168     /// let target = File::open("./target")?;
169     ///
170     /// assert_ne!(
171     ///     Handle::from_file(source)?,
172     ///     Handle::from_file(target)?,
173     ///     "The files are the same."
174     /// );
175     /// #     Ok(())
176     /// # }
177     /// #
178     /// # fn main() {
179     /// #     try_main().unwrap();
180     /// # }
181     /// ```
from_file(file: File) -> io::Result<Handle>182     pub fn from_file(file: File) -> io::Result<Handle> {
183         imp::Handle::from_file(file).map(Handle)
184     }
185 
186     /// Construct a handle from stdin.
187     ///
188     /// # Errors
189     /// This method will return an [`io::Error`] if stdin cannot
190     /// be opened due to any I/O-related reason.
191     ///
192     /// [`io::Error`]: https://doc.rust-lang.org/std/io/struct.Error.html
193     ///
194     /// # Examples
195     ///
196     /// ```rust
197     /// # use std::error::Error;
198     /// use same_file::Handle;
199     ///
200     /// # fn try_main() -> Result<(), Box<Error>> {
201     /// let stdin = Handle::stdin()?;
202     /// let stdout = Handle::stdout()?;
203     /// let stderr = Handle::stderr()?;
204     ///
205     /// if stdin == stdout {
206     ///     println!("stdin == stdout");
207     /// }
208     /// if stdin == stderr {
209     ///     println!("stdin == stderr");
210     /// }
211     /// if stdout == stderr {
212     ///     println!("stdout == stderr");
213     /// }
214     /// #
215     /// #     Ok(())
216     /// # }
217     /// #
218     /// # fn main() {
219     /// #     try_main().unwrap();
220     /// # }
221     /// ```
222     ///
223     /// The output differs depending on the platform.
224     ///
225     /// On Linux:
226     ///
227     /// ```text
228     /// $ ./example
229     /// stdin == stdout
230     /// stdin == stderr
231     /// stdout == stderr
232     /// $ ./example > result
233     /// $ cat result
234     /// stdin == stderr
235     /// $ ./example > result 2>&1
236     /// $ cat result
237     /// stdout == stderr
238     /// ```
239     ///
240     /// Windows:
241     ///
242     /// ```text
243     /// > example
244     /// > example > result 2>&1
245     /// > type result
246     /// stdout == stderr
247     /// ```
stdin() -> io::Result<Handle>248     pub fn stdin() -> io::Result<Handle> {
249         imp::Handle::stdin().map(Handle)
250     }
251 
252     /// Construct a handle from stdout.
253     ///
254     /// # Errors
255     /// This method will return an [`io::Error`] if stdout cannot
256     /// be opened due to any I/O-related reason.
257     ///
258     /// [`io::Error`]: https://doc.rust-lang.org/std/io/struct.Error.html
259     ///
260     /// # Examples
261     /// See the example for [`stdin()`].
262     ///
263     /// [`stdin()`]: #method.stdin
stdout() -> io::Result<Handle>264     pub fn stdout() -> io::Result<Handle> {
265         imp::Handle::stdout().map(Handle)
266     }
267 
268     /// Construct a handle from stderr.
269     ///
270     /// # Errors
271     /// This method will return an [`io::Error`] if stderr cannot
272     /// be opened due to any I/O-related reason.
273     ///
274     /// [`io::Error`]: https://doc.rust-lang.org/std/io/struct.Error.html
275     ///
276     /// # Examples
277     /// See the example for [`stdin()`].
278     ///
279     /// [`stdin()`]: #method.stdin
stderr() -> io::Result<Handle>280     pub fn stderr() -> io::Result<Handle> {
281         imp::Handle::stderr().map(Handle)
282     }
283 
284     /// Return a reference to the underlying file.
285     ///
286     /// # Examples
287     /// Ensure that the target file is not the same as the source one,
288     /// and copy the data to it:
289     ///
290     /// ```rust,no_run
291     /// # use std::error::Error;
292     /// use std::io::prelude::*;
293     /// use std::io::Write;
294     /// use std::fs::File;
295     /// use same_file::Handle;
296     ///
297     /// # fn try_main() -> Result<(), Box<Error>> {
298     /// let source = File::open("source")?;
299     /// let target = File::create("target")?;
300     ///
301     /// let source_handle = Handle::from_file(source)?;
302     /// let mut target_handle = Handle::from_file(target)?;
303     /// assert_ne!(source_handle, target_handle, "The files are the same.");
304     ///
305     /// let mut source = source_handle.as_file();
306     /// let target = target_handle.as_file_mut();
307     ///
308     /// let mut buffer = Vec::new();
309     /// // data copy is simplified for the purposes of the example
310     /// source.read_to_end(&mut buffer)?;
311     /// target.write_all(&buffer)?;
312     /// #
313     /// #    Ok(())
314     /// # }
315     /// #
316     /// # fn main() {
317     /// #    try_main().unwrap();
318     /// # }
319     /// ```
as_file(&self) -> &File320     pub fn as_file(&self) -> &File {
321         self.0.as_file()
322     }
323 
324     /// Return a mutable reference to the underlying file.
325     ///
326     /// # Examples
327     /// See the example for [`as_file()`].
328     ///
329     /// [`as_file()`]: #method.as_file
as_file_mut(&mut self) -> &mut File330     pub fn as_file_mut(&mut self) -> &mut File {
331         self.0.as_file_mut()
332     }
333 
334     /// Return the underlying device number of this handle.
335     ///
336     /// Note that this only works on unix platforms.
337     #[cfg(any(target_os = "redox", unix))]
dev(&self) -> u64338     pub fn dev(&self) -> u64 {
339         self.0.dev()
340     }
341 
342     /// Return the underlying inode number of this handle.
343     ///
344     /// Note that this only works on unix platforms.
345     #[cfg(any(target_os = "redox", unix))]
ino(&self) -> u64346     pub fn ino(&self) -> u64 {
347         self.0.ino()
348     }
349 }
350 
351 /// Returns true if the two file paths may correspond to the same file.
352 ///
353 /// Note that it's possible for this to produce a false positive on some
354 /// platforms. Namely, this can return true even if the two file paths *don't*
355 /// resolve to the same file.
356 /// # Errors
357 /// This function will return an [`io::Error`] if any of the two paths cannot
358 /// be opened. The most common reasons for this are: the path does not exist,
359 /// or there were not enough permissions.
360 ///
361 /// [`io::Error`]: https://doc.rust-lang.org/std/io/struct.Error.html
362 ///
363 /// # Example
364 ///
365 /// ```rust,no_run
366 /// use same_file::is_same_file;
367 ///
368 /// assert!(is_same_file("./foo", "././foo").unwrap_or(false));
369 /// ```
is_same_file<P, Q>(path1: P, path2: Q) -> io::Result<bool> where P: AsRef<Path>, Q: AsRef<Path>,370 pub fn is_same_file<P, Q>(path1: P, path2: Q) -> io::Result<bool>
371 where
372     P: AsRef<Path>,
373     Q: AsRef<Path>,
374 {
375     Ok(Handle::from_path(path1)? == Handle::from_path(path2)?)
376 }
377 
378 #[cfg(test)]
379 mod tests {
380     use std::env;
381     use std::error;
382     use std::fs::{self, File};
383     use std::io;
384     use std::path::{Path, PathBuf};
385     use std::result;
386 
387     use super::is_same_file;
388 
389     type Result<T> = result::Result<T, Box<error::Error + Send + Sync>>;
390 
391     /// Create an error from a format!-like syntax.
392     macro_rules! err {
393         ($($tt:tt)*) => {
394             Box::<error::Error + Send + Sync>::from(format!($($tt)*))
395         }
396     }
397 
398     /// A simple wrapper for creating a temporary directory that is
399     /// automatically deleted when it's dropped.
400     ///
401     /// We use this in lieu of tempfile because tempfile brings in too many
402     /// dependencies.
403     #[derive(Debug)]
404     struct TempDir(PathBuf);
405 
406     impl Drop for TempDir {
drop(&mut self)407         fn drop(&mut self) {
408             fs::remove_dir_all(&self.0).unwrap();
409         }
410     }
411 
412     impl TempDir {
413         /// Create a new empty temporary directory under the system's
414         /// configured temporary directory.
new() -> Result<TempDir>415         fn new() -> Result<TempDir> {
416             #![allow(deprecated)]
417 
418             use std::sync::atomic::{
419                 AtomicUsize, Ordering, ATOMIC_USIZE_INIT,
420             };
421 
422             static TRIES: usize = 100;
423             static COUNTER: AtomicUsize = ATOMIC_USIZE_INIT;
424 
425             let tmpdir = env::temp_dir();
426             for _ in 0..TRIES {
427                 let count = COUNTER.fetch_add(1, Ordering::SeqCst);
428                 let path = tmpdir.join("rust-walkdir").join(count.to_string());
429                 if path.is_dir() {
430                     continue;
431                 }
432                 fs::create_dir_all(&path).map_err(|e| {
433                     err!("failed to create {}: {}", path.display(), e)
434                 })?;
435                 return Ok(TempDir(path));
436             }
437             Err(err!("failed to create temp dir after {} tries", TRIES))
438         }
439 
440         /// Return the underlying path to this temporary directory.
path(&self) -> &Path441         fn path(&self) -> &Path {
442             &self.0
443         }
444     }
445 
tmpdir() -> TempDir446     fn tmpdir() -> TempDir {
447         TempDir::new().unwrap()
448     }
449 
450     #[cfg(unix)]
soft_link_dir<P: AsRef<Path>, Q: AsRef<Path>>( src: P, dst: Q, ) -> io::Result<()>451     pub fn soft_link_dir<P: AsRef<Path>, Q: AsRef<Path>>(
452         src: P,
453         dst: Q,
454     ) -> io::Result<()> {
455         use std::os::unix::fs::symlink;
456         symlink(src, dst)
457     }
458 
459     #[cfg(unix)]
soft_link_file<P: AsRef<Path>, Q: AsRef<Path>>( src: P, dst: Q, ) -> io::Result<()>460     pub fn soft_link_file<P: AsRef<Path>, Q: AsRef<Path>>(
461         src: P,
462         dst: Q,
463     ) -> io::Result<()> {
464         soft_link_dir(src, dst)
465     }
466 
467     #[cfg(windows)]
soft_link_dir<P: AsRef<Path>, Q: AsRef<Path>>( src: P, dst: Q, ) -> io::Result<()>468     pub fn soft_link_dir<P: AsRef<Path>, Q: AsRef<Path>>(
469         src: P,
470         dst: Q,
471     ) -> io::Result<()> {
472         use std::os::windows::fs::symlink_dir;
473         symlink_dir(src, dst)
474     }
475 
476     #[cfg(windows)]
soft_link_file<P: AsRef<Path>, Q: AsRef<Path>>( src: P, dst: Q, ) -> io::Result<()>477     pub fn soft_link_file<P: AsRef<Path>, Q: AsRef<Path>>(
478         src: P,
479         dst: Q,
480     ) -> io::Result<()> {
481         use std::os::windows::fs::symlink_file;
482         symlink_file(src, dst)
483     }
484 
485     // These tests are rather uninteresting. The really interesting tests
486     // would stress the edge cases. On Unix, this might be comparing two files
487     // on different mount points with the same inode number. On Windows, this
488     // might be comparing two files whose file indices are the same on file
489     // systems where such things aren't guaranteed to be unique.
490     //
491     // Alas, I don't know how to create those environmental conditions. ---AG
492 
493     #[test]
same_file_trivial()494     fn same_file_trivial() {
495         let tdir = tmpdir();
496         let dir = tdir.path();
497 
498         File::create(dir.join("a")).unwrap();
499         assert!(is_same_file(dir.join("a"), dir.join("a")).unwrap());
500     }
501 
502     #[test]
same_dir_trivial()503     fn same_dir_trivial() {
504         let tdir = tmpdir();
505         let dir = tdir.path();
506 
507         fs::create_dir(dir.join("a")).unwrap();
508         assert!(is_same_file(dir.join("a"), dir.join("a")).unwrap());
509     }
510 
511     #[test]
not_same_file_trivial()512     fn not_same_file_trivial() {
513         let tdir = tmpdir();
514         let dir = tdir.path();
515 
516         File::create(dir.join("a")).unwrap();
517         File::create(dir.join("b")).unwrap();
518         assert!(!is_same_file(dir.join("a"), dir.join("b")).unwrap());
519     }
520 
521     #[test]
not_same_dir_trivial()522     fn not_same_dir_trivial() {
523         let tdir = tmpdir();
524         let dir = tdir.path();
525 
526         fs::create_dir(dir.join("a")).unwrap();
527         fs::create_dir(dir.join("b")).unwrap();
528         assert!(!is_same_file(dir.join("a"), dir.join("b")).unwrap());
529     }
530 
531     #[test]
same_file_hard()532     fn same_file_hard() {
533         let tdir = tmpdir();
534         let dir = tdir.path();
535 
536         File::create(dir.join("a")).unwrap();
537         fs::hard_link(dir.join("a"), dir.join("alink")).unwrap();
538         assert!(is_same_file(dir.join("a"), dir.join("alink")).unwrap());
539     }
540 
541     #[test]
same_file_soft()542     fn same_file_soft() {
543         let tdir = tmpdir();
544         let dir = tdir.path();
545 
546         File::create(dir.join("a")).unwrap();
547         soft_link_file(dir.join("a"), dir.join("alink")).unwrap();
548         assert!(is_same_file(dir.join("a"), dir.join("alink")).unwrap());
549     }
550 
551     #[test]
same_dir_soft()552     fn same_dir_soft() {
553         let tdir = tmpdir();
554         let dir = tdir.path();
555 
556         fs::create_dir(dir.join("a")).unwrap();
557         soft_link_dir(dir.join("a"), dir.join("alink")).unwrap();
558         assert!(is_same_file(dir.join("a"), dir.join("alink")).unwrap());
559     }
560 
561     #[test]
test_send()562     fn test_send() {
563         fn assert_send<T: Send>() {}
564         assert_send::<super::Handle>();
565     }
566 
567     #[test]
test_sync()568     fn test_sync() {
569         fn assert_sync<T: Sync>() {}
570         assert_sync::<super::Handle>();
571     }
572 }
573