-
Notifications
You must be signed in to change notification settings - Fork 91
fix(config-traits): open read-only config files without panicking #305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,15 +83,25 @@ where | |
|
|
||
| /// Directly open the config file for read and write. If the config file | ||
| /// does not exist it is created, including the directories the file | ||
| /// resides in. | ||
| /// resides in. If the file exists but is not writable (e.g. a symlink into | ||
| /// a read-only store) it is opened read-only instead. | ||
| fn file_open(&self) -> File { | ||
| let path = self.file_path(); | ||
| OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create(true) | ||
| .truncate(false) | ||
| .open(self.file_path()) | ||
| .unwrap_or_else(|e| panic!("Could not open {:?} {e}", self.file_path())) | ||
| .open(&path) | ||
| .or_else(|e| { | ||
| if e.kind() == std::io::ErrorKind::PermissionDenied && path.exists() { | ||
| warn!("Config {path:?} is not writable, opening read-only: {e}"); | ||
| File::open(&path) | ||
| } else { | ||
| Err(e) | ||
| } | ||
| }) | ||
| .unwrap_or_else(|e| panic!("Could not open {:?} {e}", path)) | ||
| } | ||
|
|
||
| /// Open and parse the config file to self from ron format | ||
|
|
@@ -254,6 +264,45 @@ std_config_load!(StdConfigLoad4: T1, T2, T3, T4); | |
| mod tests { | ||
| use std::path::PathBuf; | ||
|
|
||
| #[test] | ||
| fn load_read_only_config() { | ||
| use crate::{StdConfig, StdConfigLoad}; | ||
|
|
||
| #[derive(serde::Deserialize, serde::Serialize, Debug)] | ||
| struct Test { | ||
| value: u32, | ||
| } | ||
|
|
||
| impl crate::StdConfig for Test { | ||
| fn new() -> Self { | ||
| Self { value: 0 } | ||
| } | ||
|
|
||
| fn file_name(&self) -> String { | ||
| "read_only_test.ron".to_owned() | ||
| } | ||
|
|
||
| fn config_dir() -> PathBuf { | ||
| std::env::temp_dir().join("config_traits_read_only_test") | ||
| } | ||
| } | ||
|
|
||
| impl crate::StdConfigLoad for Test {} | ||
|
|
||
| let dir = Test::config_dir(); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("read_only_test.ron"); | ||
| std::fs::write(&path, "(value: 7)").unwrap(); | ||
| let mut perms = std::fs::metadata(&path).unwrap().permissions(); | ||
| perms.set_readonly(true); | ||
| std::fs::set_permissions(&path, perms).unwrap(); | ||
|
|
||
| let cfg = Test::new().load(); | ||
| assert_eq!(cfg.value, 7); | ||
|
|
||
| let _ = std::fs::remove_dir_all(&dir); | ||
|
Comment on lines
+292
to
+303
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- toolchain files ---'
git ls-files | grep -E '(^|/)(rust-toolchain(\.toml)?|Cargo\.toml|Cargo\.lock)$' | head -80
printf '%s\n' '--- relevant source ---'
sed -n '70,115p' config-traits/src/lib.rs
sed -n '250,315p' config-traits/src/lib.rs
printf '%s\n' '--- error handling references ---'
rg -n 'PermissionDenied|ReadOnlyFilesystem|EROFS|file_open|load_read_only_config|config_dir' config-traits Cargo.toml */Cargo.tomlRepository: OpenGamingCollective/asusctl Length of output: 5114 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- declared Rust toolchain ---'
cat rust-toolchain.toml
printf '%s\n' '--- config-traits manifest ---'
cat config-traits/Cargo.toml
printf '%s\n' '--- workspace test dependencies and temp-dir patterns ---'
rg -n 'tempfile|temp_dir\(\)|create_dir_all|remove_dir_all|set_readonly|OpenOptions' --glob '*.rs' --glob 'Cargo.toml'
printf '%s\n' '--- relevant trait/load implementation ---'
sed -n '1,55p' config-traits/src/lib.rs
sed -n '200,245p' config-traits/src/lib.rsRepository: OpenGamingCollective/asusctl Length of output: 645 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- load/write implementation ---'
sed -n '1,115p' config-traits/src/lib.rs
sed -n '205,240p' config-traits/src/lib.rs
printf '%s\n' '--- repository temp-directory and permission usage ---'
rg -n -F 'temp_dir()' --glob '*.rs' --glob 'Cargo.toml' .
rg -n -F 'set_readonly' --glob '*.rs' --glob 'Cargo.toml' .
rg -n -F 'OpenOptions' --glob '*.rs' --glob 'Cargo.toml' .
printf '%s\n' '--- available runtime ---'
command -v rustc || true
rustc --version 2>/dev/null || true
idRepository: OpenGamingCollective/asusctl Length of output: 8567 🏁 Script executed: #!/bin/bash
set -e
python3 - <<'PY'
import errno
import os
import shutil
import stat
import tempfile
root = tempfile.mkdtemp(prefix="config-traits-review-")
path = os.path.join(root, "read_only_test.ron")
try:
with open(path, "w", encoding="utf-8") as f:
f.write("(value: 7)")
os.chmod(path, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
try:
fd = os.open(path, os.O_RDWR)
except OSError as e:
print(f"euid={os.geteuid()} writable_open=error errno={e.errno} name={errno.errorcode.get(e.errno)}")
else:
os.close(fd)
print(f"euid={os.geteuid()} writable_open=succeeded")
finally:
shutil.rmtree(root)
PYRepository: OpenGamingCollective/asusctl Length of output: 217 Make the fallback test exercise the fallback. Use a unique temporary directory. Before 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| #[test] | ||
| fn check_macro_from_1() { | ||
| #[derive(serde::Deserialize, serde::Serialize, Debug)] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
For the Rust version declared by this repository, does std::io::ErrorKind expose ReadOnlyFilesystem, and how does std::fs::OpenOptions::open map Linux EROFS errors?💡 Result:
As of August 16, 2026, the latest stable version of Rust is 1.97.1. In this version (as well as since Rust 1.83.0), std::io::ErrorKind does expose the ReadOnlyFilesystem variant [1][2][3]. Regarding how std::fs::OpenOptions::open maps Linux EROFS (Error Read-Only File System) errors: 1. Mapping: When a write operation is attempted on a read-only filesystem on Linux, the underlying operating system returns the EROFS error code. The Rust standard library translates this system error into the std::io::ErrorKind::ReadOnlyFilesystem variant [1][2]. 2. Important Nuance: While the standard library does provide this mapping, the documentation for std::fs::OpenOptions::open explicitly states that the specific mapping of operating system errors to std::io::ErrorKind variants is not part of the function's compatibility contract [4]. While it currently maps to ReadOnlyFilesystem, reliance on this exact mapping for program logic may be subject to change in future versions of the standard library [4].
Citations:
🏁 Script executed:
Repository: OpenGamingCollective/asusctl
Length of output: 6757
🏁 Script executed:
Repository: OpenGamingCollective/asusctl
Length of output: 2538
🏁 Script executed:
Repository: OpenGamingCollective/asusctl
Length of output: 3114
Handle
EROFSwithout breaking the declared MSRV.On Linux, opening a valid configuration in a read-only filesystem can return
EROFS. This branch handles onlyPermissionDenied, sofile_openpanics. AddErrorKind::ReadOnlyFilesystemand raise the workspace MSRV to 1.83, or preserve Rust 1.82 with a Unixraw_os_error() == Some(libc::EROFS)check. Keep other errors fatal.🤖 Prompt for AI Agents