Compare commits

...
2 changed files with 79 additions and 3 deletions
+62
View File
@@ -454,6 +454,50 @@ pub fn create_folder(dir: &Path) -> std::io::Result<()> {
builder.create(dir)
}
/// A folder at a predictable place in a shared folder (the system's temporary folder), for work only
/// this account may see. It is created exclusively, owner-only; one that already exists must be a
/// real folder (not a link) owned by this account and open to no one else, since another account
/// could have made it first and could read, or swap, what is written into it.
pub fn create_private_folder(dir: &Path) -> std::io::Result<()> {
if let Some(parent) = dir.parent() {
std::fs::create_dir_all(parent)?;
}
let mut builder = std::fs::DirBuilder::new();
#[cfg(unix)]
std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
match builder.create(dir) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => check_own_private_folder(dir),
Err(e) => Err(e),
}
}
#[cfg(unix)]
fn check_own_private_folder(dir: &Path) -> std::io::Result<()> {
use std::os::unix::fs::MetadataExt;
let refuse = |why: &str| Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, why.to_string()));
let meta = std::fs::symlink_metadata(dir)?;
if !meta.is_dir() {
return refuse("it exists and is not a folder");
}
if meta.mode() & 0o077 != 0 {
return refuse("it exists and is open to other accounts");
}
// This account's id, from a file it creates (std has no direct call for it).
let probe = dir.parent().unwrap_or(dir).join(format!(".soundcraft-owner-{}", std::process::id()));
let own = std::fs::OpenOptions::new().write(true).create_new(true).open(&probe).and_then(|f| f.metadata());
let _ = std::fs::remove_file(&probe);
if own?.uid() != meta.uid() {
return refuse("it exists and belongs to another account");
}
Ok(())
}
#[cfg(not(unix))]
fn check_own_private_folder(_dir: &Path) -> std::io::Result<()> {
Ok(())
}
fn write_synced(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let mut f = std::fs::File::create(path)?;
f.write_all(bytes)?;
@@ -663,6 +707,24 @@ mod tests {
let _ = std::fs::remove_dir_all(&base);
}
#[cfg(unix)]
#[test]
fn a_private_folder_is_refused_when_it_already_exists_open() {
use std::os::unix::fs::PermissionsExt;
let base = folder("private-folder");
let dir = base.join("soundcraft-recordings-1");
create_private_folder(&dir).unwrap();
create_private_folder(&dir).unwrap();
assert_eq!(std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777, 0o700);
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(create_private_folder(&dir).is_err(), "open to others");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
let link = base.join("soundcraft-recordings-2");
std::os::unix::fs::symlink(&dir, &link).unwrap();
assert!(create_private_folder(&link).is_err(), "a link is not our folder");
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn a_take_still_recording_at_a_crash_is_placed_on_recovery() {
use crate::io::Take;
+17 -3
View File
@@ -155,7 +155,13 @@ impl SoundApp {
/// autosave folder. Returns the takes it records.
fn arm_recorder(&mut self) -> Result<Vec<soundcraft_engine::io::Take>, String> {
let name = soundcraft_engine::io::sanitize_name(&self.engine.session().name);
let unsaved_dir = self.autosave_dir.as_ref().map_or_else(std::env::temp_dir, |d| d.join(name));
// Without autosave storage the takes of a session never saved go to a folder of their own under
// the shared temporary folder, made private below, not into the shared folder itself.
let shared_temp = self.autosave_dir.is_none();
let unsaved_dir = self
.autosave_dir
.as_ref()
.map_or_else(|| std::env::temp_dir().join(format!("soundcraft-recordings-{}", std::process::id())), |d| d.join(name));
let Some(r) = &mut self.recorder else { return Err("no audio input device".into()) };
let plan = soundcraft_engine::io::plan_takes(&self.engine, &unsaved_dir, r.channels);
if plan.is_empty() {
@@ -163,9 +169,17 @@ impl SoundApp {
}
// The takes are unsaved work: the folder they go into under the autosave root is
// owner-only, like the autosaves, before the recorder makes anything in it.
if self.engine.path.is_none() && self.autosave_dir.is_some() {
if self.engine.path.is_none() {
if shared_temp {
soundcraft_engine::autosave::create_private_folder(&unsaved_dir).map_err(|e| format!("{}: {e}", unsaved_dir.display()))?;
}
for dir in plan.iter().filter_map(|(_, _, path)| path.parent()) {
soundcraft_engine::autosave::create_folder(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
let made = if shared_temp {
soundcraft_engine::autosave::create_private_folder(dir)
} else {
soundcraft_engine::autosave::create_folder(dir)
};
made.map_err(|e| format!("{}: {e}", dir.display()))?;
}
}
let takes = plan