file_owner/
lib.rs

1/*!
2Set and get Unix file owner and group.
3
4UID/GUI numbers or user/group names can be used.
5
6Note: This crate will only compile on Unix systems.
7
8# Usage examples
9
10## Set owner and group by name
11
12```no_run
13use file_owner::PathExt;
14
15"/tmp/baz".set_owner("nobody").unwrap();
16"/tmp/baz".set_group("nogroup").unwrap();
17```
18
19## Set owner and group by id
20
21```no_run
22use file_owner::PathExt;
23
24"/tmp/baz".set_owner(99).unwrap();
25"/tmp/baz".set_group(99).unwrap();
26```
27
28## Get owner and group
29
30```no_run
31use file_owner::PathExt;
32
33let o = "/tmp/baz".owner().unwrap();
34o.id(); // 99
35o.name(); // Some("nobody")
36
37let g = "/tmp/baz".group().unwrap();
38g.id(); // 99
39g.name(); // Some("nogroup")
40```
41*/
42#![cfg(unix)]
43
44use nix::unistd::chown;
45use nix::unistd::{Gid, Uid, Group as NixGroup, User};
46use std::path::Path;
47use std::fmt::{self, Display};
48use std::error::Error;
49use std::convert::{TryFrom, TryInto, Infallible};
50use std::fs;
51use std::io;
52use std::os::unix::fs::MetadataExt;
53
54/// File owner or group error.
55#[derive(Debug)]
56pub enum FileOwnerError {
57    IoError(io::Error),
58    NixError(nix::Error),
59    UserNotFound(String),
60    GroupNotFound(String),
61}
62
63impl Display for FileOwnerError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            FileOwnerError::IoError(_) => write!(f, "I/O error"),
67            FileOwnerError::NixError(_) => write!(f, "*nix error"),
68            FileOwnerError::UserNotFound(name) => write!(f, "user name {:?} not found", name),
69			FileOwnerError::GroupNotFound(name) => write!(f, "group name {:?} not found", name),
70        }
71    }
72}
73
74impl Error for FileOwnerError {
75    fn source(&self) -> Option<&(dyn Error + 'static)> {
76        match self {
77            FileOwnerError::IoError(err) => Some(err),
78            FileOwnerError::NixError(err) => Some(err),
79            FileOwnerError::UserNotFound(_) => None,
80			FileOwnerError::GroupNotFound(_) => None,
81        }
82    }
83}
84
85impl From<io::Error> for FileOwnerError {
86    fn from(err: io::Error) -> FileOwnerError {
87        FileOwnerError::IoError(err)
88    }
89}
90
91impl From<nix::Error> for FileOwnerError {
92    fn from(err: nix::Error) -> FileOwnerError {
93        FileOwnerError::NixError(err)
94    }
95}
96
97impl From<Infallible> for FileOwnerError {
98    fn from(_err: Infallible) -> FileOwnerError {
99        unreachable!()
100    }
101}
102
103/// Owner of a file.
104#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
105pub struct Owner(Uid);
106
107impl Owner {
108    /// Constructs Owner from UID.
109    pub fn from_uid(uid: u32) -> Owner {
110        Owner(Uid::from_raw(uid.try_into().unwrap()))
111    }
112
113    /// Construct Owner from name.
114    pub fn from_name(user: &str) -> Result<Owner, FileOwnerError> {
115        Ok(Owner(User::from_name(user)?.ok_or_else(|| FileOwnerError::UserNotFound(user.to_owned()))?.uid))
116    }
117
118    /// Gets UID.
119    pub fn id(&self) -> u32 {
120        self.0.as_raw().try_into().unwrap()
121    }
122
123    /// Gets name if assigned to UID.
124    pub fn name(&self) -> Result<Option<String>, FileOwnerError> {
125        Ok(User::from_uid(self.0)?.map(|u| u.name))
126    }
127}
128
129impl From<u32> for Owner {
130    fn from(uid: u32) -> Owner {
131        Owner::from_uid(uid)
132    }
133}
134
135impl<'s> TryFrom<&'s str> for Owner {
136    type Error = FileOwnerError;
137
138    fn try_from(name: &'s str) -> Result<Owner, Self::Error> {
139        Owner::from_name(name)
140    }
141}
142
143impl Display for Owner {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        if let Some(name) = self.name().ok().flatten() {
146            write!(f, "{}", name)
147        } else {
148            write!(f, "{}", self.id())
149        }
150    }
151}
152
153/// Group of a file.
154#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
155pub struct Group(Gid);
156
157impl Group {
158    /// Constructs Group from GUI.
159    pub fn from_gid(gid: u32) -> Group {
160        Group(Gid::from_raw(gid.try_into().unwrap()))
161    }
162
163    /// Constructs Group from name.
164    pub fn from_name(group: &str) -> Result<Group, FileOwnerError> {
165        Ok(Group(NixGroup::from_name(group)?.ok_or_else(|| FileOwnerError::GroupNotFound(group.to_owned()))?.gid))
166    }
167
168    /// Gets GID.
169    pub fn id(&self) -> u32 {
170        self.0.as_raw().try_into().unwrap()
171    }
172
173    /// Gets name if assigned to GID.
174    pub fn name(&self) -> Result<Option<String>, FileOwnerError> {
175        Ok(NixGroup::from_gid(self.0)?.map(|u| u.name))
176    }
177}
178
179impl From<u32> for Group {
180    fn from(gid: u32) -> Group {
181        Group::from_gid(gid)
182    }
183}
184
185impl<'s> TryFrom<&'s str> for Group {
186    type Error = FileOwnerError;
187
188    fn try_from(name: &'s str) -> Result<Group, Self::Error> {
189        Group::from_name(name)
190    }
191}
192
193impl Display for Group {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        if let Some(name) = self.name().ok().flatten() {
196            write!(f, "{}", name)
197        } else {
198            write!(f, "{}", self.id())
199        }
200    }
201}
202
203/// Sets owner to file at the given path.
204pub fn set_owner<E: Into<FileOwnerError>>(path: impl AsRef<Path>, owner: impl TryInto<Owner, Error = E>) -> Result<(), FileOwnerError> {
205    Ok(chown(path.as_ref(), Some(owner.try_into().map_err(Into::into)?.0), None)?)
206}
207
208/// Sets group to file at the given path.
209pub fn set_group<E: Into<FileOwnerError>>(path: impl AsRef<Path>, group: impl TryInto<Group, Error = E>) -> Result<(), FileOwnerError> {
210    Ok(chown(path.as_ref(), None, Some(group.try_into().map_err(Into::into)?.0))?)
211}
212
213/// Sets owner and group to file at the given path.
214pub fn set_owner_group<E1: Into<FileOwnerError>, E2: Into<FileOwnerError>>(path: impl AsRef<Path>, owner: impl TryInto<Owner, Error = E1>, group: impl TryInto<Group, Error = E2>) -> Result<(), FileOwnerError> {
215    Ok(chown(path.as_ref(), Some(owner.try_into().map_err(Into::into)?.0), Some(group.try_into().map_err(Into::into)?.0))?)
216}
217
218/// Gets owner of a file at the given path.
219pub fn owner(path: impl AsRef<Path>) -> Result<Owner, FileOwnerError> {
220    Ok(Owner::from_uid(fs::metadata(path)?.uid().try_into().unwrap()))
221}
222
223/// Gets group of a file at the given path.
224pub fn group(path: impl AsRef<Path>) -> Result<Group, FileOwnerError> {
225    Ok(Group::from_gid(fs::metadata(path)?.gid().try_into().unwrap()))
226}
227
228/// Gets owner and group of a file at the given path.
229pub fn owner_group(path: impl AsRef<Path>) -> Result<(Owner, Group), FileOwnerError> {
230    let meta = fs::metadata(path)?;
231    Ok((Owner::from_uid(meta.uid().try_into().unwrap()), Group::from_gid(meta.gid().try_into().unwrap())))
232}
233
234/// Extension methods for `T: AsRef<Path>`.
235pub trait PathExt {
236    /// Sets owner to file at the given path.
237    fn set_owner<E: Into<FileOwnerError>>(&self, owner: impl TryInto<Owner, Error = E>) -> Result<(), FileOwnerError>;
238
239    /// Sets group to file at the given path.
240    fn set_group<E: Into<FileOwnerError>>(&self, group: impl TryInto<Group, Error = E>) -> Result<(), FileOwnerError>;
241
242    /// Sets owner and group to file at the given path.
243    fn set_owner_group<E1: Into<FileOwnerError>, E2: Into<FileOwnerError>>(&self, owner: impl TryInto<Owner, Error = E1>, group: impl TryInto<Group, Error = E2>) -> Result<(), FileOwnerError>;
244
245    /// Gets owner of a file at the given path.
246    fn owner(&self) -> Result<Owner, FileOwnerError>;
247
248    /// Gets group of a file at the given path.
249    fn group(&self) -> Result<Group, FileOwnerError>;
250
251    /// Gets owner and group of a file at the given path.
252    fn owner_group(&self) -> Result<(Owner, Group), FileOwnerError>;
253}
254
255impl<T: AsRef<Path>> PathExt for T {
256    fn set_owner<E: Into<FileOwnerError>>(&self, owner: impl TryInto<Owner, Error = E>) -> Result<(), FileOwnerError> {
257        set_owner(self, owner)
258    }
259
260    fn set_group<E: Into<FileOwnerError>>(&self, group: impl TryInto<Group, Error = E>) -> Result<(), FileOwnerError> {
261        set_group(self, group)
262    }
263
264    fn set_owner_group<E1: Into<FileOwnerError>, E2: Into<FileOwnerError>>(&self, owner: impl TryInto<Owner, Error = E1>, group: impl TryInto<Group, Error = E2>) -> Result<(), FileOwnerError> {
265        set_owner_group(self, owner, group)
266    }
267
268    fn owner(&self) -> Result<Owner, FileOwnerError> {
269        owner(self)
270    }
271
272    fn group(&self) -> Result<Group, FileOwnerError> {
273        group(self)
274    }
275
276    fn owner_group(&self) -> Result<(Owner, Group), FileOwnerError> {
277        owner_group(self)
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_display() {
287        let nobody_id = Owner::from_name("nobody").unwrap().id();
288        let nogroup_id = Group::from_name("nogroup").unwrap().id();
289
290        assert_eq!(&Owner::from_uid(nobody_id).to_string(), "nobody");
291        assert_eq!(&Group::from_gid(nogroup_id).to_string(), "nogroup");
292
293        assert_eq!(&Owner::from_uid(321321).to_string(), "321321");
294        assert_eq!(&Group::from_gid(321321).to_string(), "321321");
295    }
296
297    #[test]
298    #[ignore]
299    fn test_set_get() {
300        let nobody_id = Owner::from_name("nobody").unwrap().id();
301        let nogroup_id = Group::from_name("nogroup").unwrap().id();
302
303        let file1 = tempfile::NamedTempFile::new().unwrap();
304        let file_path1 = file1.path();
305        let file2 = tempfile::NamedTempFile::new().unwrap();
306        let file_path2 = file2.path();
307
308        set_owner(file_path1, "nobody").unwrap();
309        assert_eq!(owner(file_path1).unwrap().name().unwrap().as_deref(), Some("nobody"));
310        set_owner(file_path2, nobody_id).unwrap();
311        assert_eq!(owner(file_path2).unwrap().name().unwrap().as_deref(), Some("nobody"));
312
313        set_group(file_path1, "nogroup").unwrap();
314        assert_eq!(group(file_path1).unwrap().name().unwrap().as_deref(), Some("nogroup"));
315        set_group(file_path2, nogroup_id).unwrap();
316        assert_eq!(group(file_path2).unwrap().name().unwrap().as_deref(), Some("nogroup"));
317    }
318
319    #[test]
320    #[ignore]
321    fn test_set_get_all() {
322        let nobody_id = Owner::from_name("nobody").unwrap().id();
323        let nogroup_id = Group::from_name("nogroup").unwrap().id();
324
325        let file1 = tempfile::NamedTempFile::new().unwrap();
326        let file_path1 = file1.path();
327        let file2 = tempfile::NamedTempFile::new().unwrap();
328        let file_path2 = file2.path();
329        let file3 = tempfile::NamedTempFile::new().unwrap();
330        let file_path3 = file3.path();
331        let file4 = tempfile::NamedTempFile::new().unwrap();
332        let file_path4 = file4.path();
333
334        set_owner_group(file_path1, "nobody", "nogroup").unwrap();
335        assert_eq!(owner(file_path1).unwrap().name().unwrap().as_deref(), Some("nobody"));
336        assert_eq!(group(file_path1).unwrap().name().unwrap().as_deref(), Some("nogroup"));
337        set_owner_group(file_path2, nobody_id, nogroup_id).unwrap();
338        assert_eq!(owner(file_path2).unwrap().name().unwrap().as_deref(), Some("nobody"));
339        assert_eq!(group(file_path2).unwrap().name().unwrap().as_deref(), Some("nogroup"));
340
341        set_owner_group(file_path3, nobody_id, "nogroup").unwrap();
342        assert_eq!(owner(file_path3).unwrap().name().unwrap().as_deref(), Some("nobody"));
343        assert_eq!(group(file_path3).unwrap().name().unwrap().as_deref(), Some("nogroup"));
344        set_owner_group(file_path4, "nobody", nogroup_id).unwrap();
345        assert_eq!(owner(file_path4).unwrap().name().unwrap().as_deref(), Some("nobody"));
346        assert_eq!(group(file_path4).unwrap().name().unwrap().as_deref(), Some("nogroup"));
347    }
348
349    #[test]
350    #[ignore]
351    fn test_get_all() {
352        let file = tempfile::NamedTempFile::new().unwrap();
353        let file_path = file.path();
354
355        set_owner_group(file_path, "nobody", "nogroup").unwrap();
356
357        let (o, g) = owner_group(file_path).unwrap();
358        assert_eq!(o.name().unwrap().as_deref(), Some("nobody"));
359        assert_eq!(g.name().unwrap().as_deref(), Some("nogroup"));
360    }
361
362    #[test]
363    #[ignore]
364    fn test_ext_traits() {
365        let nobody_id = Owner::from_name("nobody").unwrap().id();
366        let nogroup_id = Group::from_name("nogroup").unwrap().id();
367
368        let file = tempfile::NamedTempFile::new().unwrap();
369        let file_path = file.path();
370
371        file_path.set_owner("nobody").unwrap();
372        file_path.set_group("nogroup").unwrap();
373
374        assert_eq!(file_path.owner().unwrap().name().unwrap().as_deref(), Some("nobody"));
375        assert_eq!(file_path.group().unwrap().name().unwrap().as_deref(), Some("nogroup"));
376
377        file_path.set_owner_group("nobody", "nogroup").unwrap();
378
379        let (o, g) = file_path.owner_group().unwrap();
380        assert_eq!(o.name().unwrap().as_deref(), Some("nobody"));
381        assert_eq!(g.name().unwrap().as_deref(), Some("nogroup"));
382
383        assert_eq!(o.id(), nobody_id);
384        assert_eq!(g.id(), nogroup_id);
385    }
386}