Skip to content

Commit 6a534c5

Browse files
Expose chown functionality to FileSystem trait (#164)
This PR adds `chown` functionality to the LiteBox filesystem API, following the same pattern as the existing `chmod` implementation. Fixes #163. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jaybosamiya-ms <171180729+jaybosamiya-ms@users.noreply.github.com> Co-authored-by: Jay Bosamiya (Microsoft) <jayb@microsoft.com>
1 parent 9ed37cf commit 6a534c5

File tree

8 files changed

+228
-10
lines changed

8 files changed

+228
-10
lines changed

litebox/src/fs/devices/stdio.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use crate::{
88
fs::{
99
FileStatus, FileType, Mode, OFlags, SeekWhence,
1010
errors::{
11-
ChmodError, CloseError, FileStatusError, MkdirError, OpenError, PathError, ReadError,
12-
RmdirError, SeekError, UnlinkError, WriteError,
11+
ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError,
12+
ReadError, RmdirError, SeekError, UnlinkError, WriteError,
1313
},
1414
},
1515
path::Arg,
@@ -136,6 +136,15 @@ impl<Platform: crate::platform::StdioProvider> super::super::FileSystem for File
136136
unimplemented!()
137137
}
138138

139+
fn chown(
140+
&self,
141+
path: impl Arg,
142+
user: Option<u16>,
143+
group: Option<u16>,
144+
) -> Result<(), ChownError> {
145+
unimplemented!()
146+
}
147+
139148
fn unlink(&self, path: impl Arg) -> Result<(), UnlinkError> {
140149
unimplemented!()
141150
}

litebox/src/fs/errors.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ pub enum ChmodError {
7272
PathError(#[from] PathError),
7373
}
7474

75+
/// Possible errors from [`FileSystem::chown`]
76+
#[non_exhaustive]
77+
#[derive(Error, Debug)]
78+
pub enum ChownError {
79+
#[error(
80+
"the effective UID does not match the owner of the file, \
81+
and the process is not privileged"
82+
)]
83+
NotTheOwner,
84+
#[error("the named file resides on a read-only filesystem")]
85+
ReadOnlyFileSystem,
86+
#[error(transparent)]
87+
PathError(#[from] PathError),
88+
}
89+
7590
/// Possible errors from [`FileSystem::unlink`]
7691
#[non_exhaustive]
7792
#[derive(Error, Debug)]

litebox/src/fs/in_mem.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use crate::path::Arg;
1111
use crate::sync;
1212

1313
use super::errors::{
14-
ChmodError, CloseError, FileStatusError, MetadataError, MkdirError, OpenError, PathError,
15-
ReadError, RmdirError, SeekError, SetMetadataError, UnlinkError, WriteError,
14+
ChmodError, ChownError, CloseError, FileStatusError, MetadataError, MkdirError, OpenError,
15+
PathError, ReadError, RmdirError, SeekError, SetMetadataError, UnlinkError, WriteError,
1616
};
1717
use super::{FileStatus, Mode, SeekWhence};
1818
use crate::utilities::anymap::AnyMap;
@@ -74,6 +74,21 @@ impl<Platform: sync::RawSyncPrimitivesProvider> FileSystem<Platform> {
7474
unreachable!()
7575
}
7676
}
77+
78+
/// Execute `f` as a specific user (for testing purposes).
79+
#[cfg(test)]
80+
pub fn with_user<F>(&mut self, user: u16, group: u16, f: F)
81+
where
82+
F: FnOnce(&mut Self),
83+
{
84+
let test_user = UserInfo { user, group };
85+
let original_user = core::mem::replace(&mut self.current_user, test_user);
86+
f(self);
87+
let test_user_again = core::mem::replace(&mut self.current_user, original_user);
88+
if test_user_again.user != test_user.user || test_user_again.group != test_user.group {
89+
unreachable!()
90+
}
91+
}
7792
}
7893

7994
impl<Platform: sync::RawSyncPrimitivesProvider> super::private::Sealed for FileSystem<Platform> {}
@@ -313,6 +328,48 @@ impl<Platform: sync::RawSyncPrimitivesProvider> super::FileSystem for FileSystem
313328
}
314329
}
315330

331+
fn chown(
332+
&self,
333+
path: impl crate::path::Arg,
334+
user: Option<u16>,
335+
group: Option<u16>,
336+
) -> Result<(), ChownError> {
337+
let path = self.absolute_path(path)?;
338+
let mut root = self.root.write();
339+
let (_, entry) = root.parent_and_entry(&path, self.current_user)?;
340+
let Some(entry) = entry else {
341+
return Err(PathError::NoSuchFileOrDirectory)?;
342+
};
343+
match entry {
344+
Entry::File(file) => {
345+
let perms = &mut file.write().perms;
346+
if !(self.current_user.user == 0 || self.current_user.user == perms.userinfo.user) {
347+
return Err(ChownError::NotTheOwner);
348+
}
349+
if let Some(new_user) = user {
350+
perms.userinfo.user = new_user;
351+
}
352+
if let Some(new_group) = group {
353+
perms.userinfo.group = new_group;
354+
}
355+
Ok(())
356+
}
357+
Entry::Dir(dir) => {
358+
let perms = &mut dir.write().perms;
359+
if !(self.current_user.user == 0 || self.current_user.user == perms.userinfo.user) {
360+
return Err(ChownError::NotTheOwner);
361+
}
362+
if let Some(new_user) = user {
363+
perms.userinfo.user = new_user;
364+
}
365+
if let Some(new_group) = group {
366+
perms.userinfo.group = new_group;
367+
}
368+
Ok(())
369+
}
370+
}
371+
}
372+
316373
fn unlink(&self, path: impl crate::path::Arg) -> Result<(), UnlinkError> {
317374
let path = self.absolute_path(path)?;
318375
let mut root = self.root.write();

litebox/src/fs/layered.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use crate::path::Arg;
1111
use crate::sync;
1212

1313
use super::errors::{
14-
ChmodError, CloseError, FileStatusError, MkdirError, OpenError, PathError, ReadError,
15-
RmdirError, SeekError, UnlinkError, WriteError,
14+
ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError,
15+
ReadError, RmdirError, SeekError, UnlinkError, WriteError,
1616
};
1717
use super::{FileStatus, FileType, Mode, OFlags, SeekWhence};
1818

@@ -623,6 +623,44 @@ impl<Platform: sync::RawSyncPrimitivesProvider, Upper: super::FileSystem, Lower:
623623
self.chmod(path, mode)
624624
}
625625

626+
fn chown(
627+
&self,
628+
path: impl crate::path::Arg,
629+
user: Option<u16>,
630+
group: Option<u16>,
631+
) -> Result<(), ChownError> {
632+
let path = self.absolute_path(path)?;
633+
match self.upper.chown(path.as_str(), user, group) {
634+
Ok(()) => return Ok(()),
635+
Err(e) => match e {
636+
ChownError::NotTheOwner
637+
| ChownError::ReadOnlyFileSystem
638+
| ChownError::PathError(
639+
PathError::ComponentNotADirectory
640+
| PathError::InvalidPathname
641+
| PathError::NoSearchPerms { .. },
642+
) => {
643+
return Err(e);
644+
}
645+
ChownError::PathError(
646+
PathError::NoSuchFileOrDirectory | PathError::MissingComponent,
647+
) => {
648+
// fallthrough
649+
}
650+
},
651+
}
652+
self.ensure_lower_contains(&path)?;
653+
match self.migrate_file_up(&path) {
654+
Ok(()) => {}
655+
Err(MigrationError::NoReadPerms) => unimplemented!(),
656+
Err(MigrationError::NotAFile) => unimplemented!(),
657+
Err(MigrationError::PathError(e)) => unreachable!(),
658+
}
659+
// Since it has been migrated, we can just re-trigger, causing it to apply to the
660+
// upper layer
661+
self.chown(path, user, group)
662+
}
663+
626664
fn unlink(&self, path: impl crate::path::Arg) -> Result<(), UnlinkError> {
627665
let path = self.absolute_path(path)?;
628666
match self.upper.unlink(path.as_str()) {

litebox/src/fs/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ pub mod tar_ro;
1919
mod tests;
2020

2121
use errors::{
22-
ChmodError, CloseError, FileStatusError, MetadataError, MkdirError, OpenError, ReadError,
23-
RmdirError, SeekError, SetMetadataError, UnlinkError, WriteError,
22+
ChmodError, ChownError, CloseError, FileStatusError, MetadataError, MkdirError, OpenError,
23+
ReadError, RmdirError, SeekError, SetMetadataError, UnlinkError, WriteError,
2424
};
2525

2626
/// A private module, to help support writing sealed traits. This module should _itself_ never be
@@ -64,6 +64,13 @@ pub trait FileSystem: private::Sealed {
6464
fn seek(&self, fd: &FileFd, offset: isize, whence: SeekWhence) -> Result<usize, SeekError>;
6565
/// Change the permissions of a file
6666
fn chmod(&self, path: impl path::Arg, mode: Mode) -> Result<(), ChmodError>;
67+
/// Change the owner of a file
68+
fn chown(
69+
&self,
70+
path: impl path::Arg,
71+
user: Option<u16>,
72+
group: Option<u16>,
73+
) -> Result<(), ChownError>;
6774
/// Unlink a file
6875
fn unlink(&self, path: impl path::Arg) -> Result<(), UnlinkError>;
6976
/// Create a new directory

litebox/src/fs/nine_p.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ impl<Platform: platform::Provider> super::FileSystem for FileSystem<Platform> {
7474
todo!()
7575
}
7676

77+
fn chown(
78+
&self,
79+
path: impl crate::path::Arg,
80+
user: Option<u16>,
81+
group: Option<u16>,
82+
) -> Result<(), super::errors::ChownError> {
83+
todo!()
84+
}
85+
7786
fn unlink(&self, path: impl crate::path::Arg) -> Result<(), super::errors::UnlinkError> {
7887
todo!()
7988
}

litebox/src/fs/tar_ro.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ use crate::{LiteBox, path::Arg as _, sync, utilities::anymap::AnyMap};
3131
use super::{
3232
Mode, OFlags, SeekWhence,
3333
errors::{
34-
ChmodError, CloseError, MkdirError, OpenError, PathError, ReadError, RmdirError, SeekError,
35-
UnlinkError, WriteError,
34+
ChmodError, ChownError, CloseError, MkdirError, OpenError, PathError, ReadError,
35+
RmdirError, SeekError, UnlinkError, WriteError,
3636
},
3737
};
3838

@@ -244,6 +244,29 @@ impl<Platform: sync::RawSyncPrimitivesProvider> super::FileSystem for FileSystem
244244
}
245245
}
246246

247+
fn chown(
248+
&self,
249+
path: impl crate::path::Arg,
250+
user: Option<u16>,
251+
group: Option<u16>,
252+
) -> Result<(), ChownError> {
253+
let path = self.absolute_path(path)?;
254+
assert!(path.starts_with('/'));
255+
let path = &path[1..];
256+
if self
257+
.tar_data
258+
.entries()
259+
.any(|entry| match entry.filename().as_str() {
260+
Ok(p) => p == path || contains_dir(p, path),
261+
Err(_) => false,
262+
})
263+
{
264+
Err(ChownError::ReadOnlyFileSystem)
265+
} else {
266+
Err(PathError::NoSuchFileOrDirectory)?
267+
}
268+
}
269+
247270
fn unlink(&self, path: impl crate::path::Arg) -> Result<(), UnlinkError> {
248271
let path = self.absolute_path(path)?;
249272
assert!(path.starts_with('/'));

litebox/src/fs/tests.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,66 @@ mod in_mem {
161161
"Directory should not exist"
162162
);
163163
}
164+
165+
#[test]
166+
fn chown_test() {
167+
let litebox = LiteBox::new(MockPlatform::new());
168+
let mut fs = in_mem::FileSystem::new(&litebox);
169+
170+
// Create a test file as root
171+
fs.with_root_privileges(|fs| {
172+
let path = "/testfile";
173+
let fd = fs
174+
.open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU)
175+
.expect("Failed to create file");
176+
fs.close(fd).expect("Failed to close file");
177+
178+
// First chown to 1000:1000 as root (should succeed)
179+
fs.chown(path, Some(1000), Some(1000))
180+
.expect("Failed to chown as root");
181+
});
182+
183+
// Switch to user 1000 and test that owner can chown (should succeed)
184+
let path = "/testfile";
185+
fs.with_user(1000, 1000, |fs| {
186+
fs.chown(path, Some(123), Some(456))
187+
.expect("Failed to chown as owner");
188+
});
189+
190+
// Switch to a different user and test that non-owner cannot chown (should fail)
191+
fs.with_user(500, 500, |fs| {
192+
match fs.chown(path, Some(789), Some(101)) {
193+
Err(crate::fs::errors::ChownError::NotTheOwner) => {
194+
// Expected behavior
195+
}
196+
Ok(()) => panic!("Non-owner should not be able to chown"),
197+
Err(e) => panic!("Unexpected error: {:?}", e),
198+
}
199+
});
200+
201+
// Test chown on non-existent file (should fail)
202+
match fs.chown("/nonexistent", Some(123), Some(456)) {
203+
Err(crate::fs::errors::ChownError::PathError(
204+
crate::fs::errors::PathError::NoSuchFileOrDirectory,
205+
)) => {
206+
// Expected behavior
207+
}
208+
Ok(()) => panic!("Should not be able to chown non-existent file"),
209+
Err(e) => panic!("Unexpected error: {:?}", e),
210+
}
211+
212+
// Test partial chown (change only user, leave group unchanged)
213+
fs.with_root_privileges(|fs| {
214+
fs.chown(path, Some(999), None)
215+
.expect("Failed to chown user only");
216+
});
217+
218+
// Test partial chown (change only group, leave user unchanged)
219+
fs.with_root_privileges(|fs| {
220+
fs.chown(path, None, Some(888))
221+
.expect("Failed to chown group only");
222+
});
223+
}
164224
}
165225

166226
mod tar_ro {

0 commit comments

Comments
 (0)