77//! A [`Filesystem`] struct represents a device containing a
88//! filesystem mounted at a particular directory. It also includes
99//! information on amount of space available and amount of space used.
10- # [ cfg ( windows ) ]
10+ // spell-checker:ignore canonicalized
1111use std:: path:: Path ;
1212
1313#[ cfg( unix) ]
@@ -30,6 +30,40 @@ pub(crate) struct Filesystem {
3030 pub usage : FsUsage ,
3131}
3232
33+ /// Find the mount info that best matches a given filesystem path.
34+ ///
35+ /// This function returns the element of `mounts` on which `path` is
36+ /// mounted. If there are no matches, this function returns
37+ /// [`None`]. If there are two or more matches, then the single
38+ /// [`MountInfo`] with the longest mount directory is returned.
39+ ///
40+ /// If `canonicalize` is `true`, then the `path` is canonicalized
41+ /// before checking whether it matches any mount directories.
42+ ///
43+ /// # See also
44+ ///
45+ /// * [`Path::canonicalize`]
46+ /// * [`MountInfo::mount_dir`]
47+ fn mount_info_from_path < P > (
48+ mounts : & [ MountInfo ] ,
49+ path : P ,
50+ // This is really only used for testing purposes.
51+ canonicalize : bool ,
52+ ) -> Option < & MountInfo >
53+ where
54+ P : AsRef < Path > ,
55+ {
56+ // TODO Refactor this function with `Stater::find_mount_point()`
57+ // in the `stat` crate.
58+ let path = if canonicalize {
59+ path. as_ref ( ) . canonicalize ( ) . ok ( ) ?
60+ } else {
61+ path. as_ref ( ) . to_path_buf ( )
62+ } ;
63+ let matches = mounts. iter ( ) . filter ( |mi| path. starts_with ( & mi. mount_dir ) ) ;
64+ matches. max_by_key ( |mi| mi. mount_dir . len ( ) )
65+ }
66+
3367impl Filesystem {
3468 // TODO: resolve uuid in `mount_info.dev_name` if exists
3569 pub ( crate ) fn new ( mount_info : MountInfo ) -> Option < Self > {
@@ -52,4 +86,106 @@ impl Filesystem {
5286 let usage = FsUsage :: new ( Path :: new ( & _stat_path) ) ;
5387 Some ( Self { mount_info, usage } )
5488 }
89+
90+ /// Find and create the filesystem that best matches a given path.
91+ ///
92+ /// This function returns a new `Filesystem` derived from the
93+ /// element of `mounts` on which `path` is mounted. If there are
94+ /// no matches, this function returns [`None`]. If there are two
95+ /// or more matches, then the single [`Filesystem`] with the
96+ /// longest mount directory is returned.
97+ ///
98+ /// The `path` is canonicalized before checking whether it matches
99+ /// any mount directories.
100+ ///
101+ /// # See also
102+ ///
103+ /// * [`Path::canonicalize`]
104+ /// * [`MountInfo::mount_dir`]
105+ ///
106+ pub ( crate ) fn from_path < P > ( mounts : & [ MountInfo ] , path : P ) -> Option < Self >
107+ where
108+ P : AsRef < Path > ,
109+ {
110+ let canonicalize = true ;
111+ let mount_info = mount_info_from_path ( mounts, path, canonicalize) ?;
112+ // TODO Make it so that we do not need to clone the `mount_info`.
113+ let mount_info = ( * mount_info) . clone ( ) ;
114+ Self :: new ( mount_info)
115+ }
116+ }
117+
118+ #[ cfg( test) ]
119+ mod tests {
120+
121+ mod mount_info_from_path {
122+
123+ use uucore:: fsext:: MountInfo ;
124+
125+ use crate :: filesystem:: mount_info_from_path;
126+
127+ // Create a fake `MountInfo` with the given directory name.
128+ fn mount_info ( mount_dir : & str ) -> MountInfo {
129+ MountInfo {
130+ dev_id : Default :: default ( ) ,
131+ dev_name : Default :: default ( ) ,
132+ fs_type : Default :: default ( ) ,
133+ mount_dir : String :: from ( mount_dir) ,
134+ mount_option : Default :: default ( ) ,
135+ mount_root : Default :: default ( ) ,
136+ remote : Default :: default ( ) ,
137+ dummy : Default :: default ( ) ,
138+ }
139+ }
140+
141+ // Check whether two `MountInfo` instances are equal.
142+ fn mount_info_eq ( m1 : & MountInfo , m2 : & MountInfo ) -> bool {
143+ m1. dev_id == m2. dev_id
144+ && m1. dev_name == m2. dev_name
145+ && m1. fs_type == m2. fs_type
146+ && m1. mount_dir == m2. mount_dir
147+ && m1. mount_option == m2. mount_option
148+ && m1. mount_root == m2. mount_root
149+ && m1. remote == m2. remote
150+ && m1. dummy == m2. dummy
151+ }
152+
153+ #[ test]
154+ fn test_empty_mounts ( ) {
155+ assert ! ( mount_info_from_path( & [ ] , "/" , false ) . is_none( ) ) ;
156+ }
157+
158+ #[ test]
159+ fn test_exact_match ( ) {
160+ let mounts = [ mount_info ( "/foo" ) ] ;
161+ let actual = mount_info_from_path ( & mounts, "/foo" , false ) . unwrap ( ) ;
162+ assert ! ( mount_info_eq( actual, & mounts[ 0 ] ) ) ;
163+ }
164+
165+ #[ test]
166+ fn test_prefix_match ( ) {
167+ let mounts = [ mount_info ( "/foo" ) ] ;
168+ let actual = mount_info_from_path ( & mounts, "/foo/bar" , false ) . unwrap ( ) ;
169+ assert ! ( mount_info_eq( actual, & mounts[ 0 ] ) ) ;
170+ }
171+
172+ #[ test]
173+ fn test_multiple_matches ( ) {
174+ let mounts = [ mount_info ( "/foo" ) , mount_info ( "/foo/bar" ) ] ;
175+ let actual = mount_info_from_path ( & mounts, "/foo/bar" , false ) . unwrap ( ) ;
176+ assert ! ( mount_info_eq( actual, & mounts[ 1 ] ) ) ;
177+ }
178+
179+ #[ test]
180+ fn test_no_match ( ) {
181+ let mounts = [ mount_info ( "/foo" ) ] ;
182+ assert ! ( mount_info_from_path( & mounts, "/bar" , false ) . is_none( ) ) ;
183+ }
184+
185+ #[ test]
186+ fn test_partial_match ( ) {
187+ let mounts = [ mount_info ( "/foo/bar" ) ] ;
188+ assert ! ( mount_info_from_path( & mounts, "/foo/baz" , false ) . is_none( ) ) ;
189+ }
190+ }
55191}
0 commit comments