use super::SqliteStore;
use super::store_perm::{create_data_dir, prepare_db_files, tighten_as};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
struct ModeGuard {
path: PathBuf,
mode: u32,
}
impl Drop for ModeGuard {
fn drop(&mut self) {
let _ = fs::set_permissions(&self.path, fs::Permissions::from_mode(self.mode));
}
}
fn mode_of(path: &Path) -> u32 {
fs::metadata(path).unwrap().permissions().mode() & 0o777
}
fn set_mode(path: &Path, mode: u32) {
fs::set_permissions(path, fs::Permissions::from_mode(mode)).unwrap();
}
#[cfg(unix)]
#[test]
fn test_create_data_dir_makes_leaf_private_and_creates_missing_parents() {
let tmp = TempDir::new().unwrap();
let parent = tmp.path().join("outer");
set_mode(tmp.path(), 0o755);
let dir = parent.join("oo");
create_data_dir(&dir).unwrap();
assert_eq!(mode_of(&dir), 0o700);
assert!(parent.is_dir(), "missing parents must still be created");
}
#[cfg(unix)]
#[test]
fn test_create_data_dir_keeps_mode_of_existing_directory() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("existing");
fs::create_dir(&dir).unwrap();
set_mode(&dir, 0o755);
create_data_dir(&dir).unwrap();
assert_eq!(
mode_of(&dir),
0o755,
"a pre-existing directory is not chmod-ed"
);
}
#[cfg(unix)]
#[test]
fn test_open_at_creates_private_dir_and_db_file() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join(".oo");
let db = dir.join("oo.db");
let _store = SqliteStore::open_at(&db).unwrap();
assert_eq!(mode_of(&dir), 0o700);
assert_eq!(mode_of(&db), 0o600);
}
#[cfg(unix)]
#[test]
fn test_open_at_tightens_existing_world_readable_db() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("oo.db");
fs::write(&db, b"").unwrap();
set_mode(&db, 0o644);
let _store = SqliteStore::open_at(&db).unwrap();
assert_eq!(mode_of(&db), 0o600);
}
#[cfg(unix)]
#[test]
fn test_prepare_db_files_tightens_leftover_sidecars() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("oo.db");
fs::write(&db, b"").unwrap();
let wal = tmp.path().join("oo.db-wal");
let shm = tmp.path().join("oo.db-shm");
fs::write(&wal, b"").unwrap();
fs::write(&shm, b"").unwrap();
set_mode(&db, 0o644);
set_mode(&wal, 0o644);
set_mode(&shm, 0o644);
prepare_db_files(&db);
assert_eq!(mode_of(&db), 0o600);
assert_eq!(mode_of(&wal), 0o600);
assert_eq!(mode_of(&shm), 0o600);
}
#[cfg(unix)]
#[test]
fn test_prepare_db_files_leaves_already_private_file_alone() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("oo.db");
fs::write(&db, b"").unwrap();
set_mode(&db, 0o400);
prepare_db_files(&db);
assert_eq!(mode_of(&db), 0o400, "owner-only modes are not rewritten");
}
#[cfg(unix)]
#[test]
fn test_prepare_db_files_does_not_truncate_existing_db() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("oo.db");
fs::write(&db, b"keep me").unwrap();
prepare_db_files(&db);
assert_eq!(fs::read(&db).unwrap(), b"keep me");
}
#[cfg(unix)]
#[test]
fn test_tighten_as_leaves_file_owned_by_another_user_alone() {
use std::os::unix::fs::MetadataExt;
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("oo.db");
fs::write(&db, b"").unwrap();
set_mode(&db, 0o644);
let owner = fs::metadata(&db).unwrap().uid();
tighten_as(&db, owner.wrapping_add(1));
assert_eq!(
mode_of(&db),
0o644,
"a file owned by another uid is not chmod-ed"
);
tighten_as(&db, owner);
assert_eq!(
mode_of(&db),
0o600,
"the owning uid does get the tightening"
);
}
#[cfg(unix)]
#[test]
fn test_wal_sidecars_are_private_while_connection_is_open() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("oo.db");
let store = SqliteStore::open_at(&db).unwrap();
store
.conn
.pragma_update(None, "journal_mode", "WAL")
.unwrap();
store
.conn
.execute_batch("CREATE TABLE probe (x INTEGER); INSERT INTO probe VALUES (1);")
.unwrap();
for sidecar in ["oo.db-wal", "oo.db-shm"] {
let path = tmp.path().join(sidecar);
assert!(path.exists(), "{sidecar} should exist in WAL mode");
assert_eq!(mode_of(&path), 0o600, "{sidecar} must be private");
}
}
#[cfg(unix)]
#[test]
fn test_prepare_db_files_does_not_follow_symlink() {
use std::os::unix::fs::symlink;
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("elsewhere.txt");
fs::write(&target, b"x").unwrap();
set_mode(&target, 0o644);
let db = tmp.path().join("oo.db");
symlink(&target, &db).unwrap();
prepare_db_files(&db);
assert_eq!(
mode_of(&target),
0o644,
"a symlink target is never chmod-ed"
);
}
#[cfg(unix)]
#[test]
fn test_journal_created_during_write_is_private() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("oo.db");
let store = SqliteStore::open_at(&db).unwrap();
store
.conn
.execute_batch(
"BEGIN IMMEDIATE; CREATE TABLE probe (x INTEGER); INSERT INTO probe VALUES (1);",
)
.unwrap();
let journal = tmp.path().join("oo.db-journal");
assert!(journal.exists(), "journal should exist mid-transaction");
assert_eq!(mode_of(&journal), 0o600);
store.conn.execute_batch("COMMIT;").unwrap();
}
#[test]
fn test_open_at_through_symlink_leaves_target_mode_and_still_works() {
use std::os::unix::fs::symlink;
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("elsewhere.db");
fs::write(&target, b"").unwrap();
set_mode(&target, 0o644);
let db = tmp.path().join("oo.db");
symlink(&target, &db).unwrap();
let store = SqliteStore::open_at(&db).expect("open_at must succeed through a symlink");
store
.conn
.execute_batch(
"BEGIN IMMEDIATE; CREATE TABLE probe (x INTEGER); INSERT INTO probe VALUES (1);",
)
.unwrap();
let journal = tmp.path().join("elsewhere.db-journal");
assert!(journal.exists(), "journal should exist mid-transaction");
assert_eq!(
mode_of(&journal),
0o644,
"a symlinked oo.db journal takes the target's mode"
);
store.conn.execute_batch("COMMIT;").unwrap();
assert_eq!(
mode_of(&target),
0o644,
"the symlink target is never chmod-ed"
);
}
#[test]
fn test_open_at_in_existing_read_only_dir_still_opens_and_tightens_db() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("data");
let db = dir.join("oo.db");
drop(SqliteStore::open_at(&db).unwrap());
set_mode(&db, 0o644);
let _restore = ModeGuard {
path: dir.clone(),
mode: 0o700,
};
set_mode(&dir, 0o500);
let store = SqliteStore::open_at(&db).expect("open_at must not fail on a read-only dir");
let rows: i64 = store
.conn
.query_row("SELECT COUNT(*) FROM entries", [], |row| row.get(0))
.unwrap();
assert_eq!(rows, 0);
assert_eq!(mode_of(&db), 0o600);
assert_eq!(
mode_of(&dir),
0o500,
"an existing directory is not chmod-ed"
);
}
#[test]
fn test_prepare_db_files_never_panics_when_paths_cannot_be_changed() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("missing").join("oo.db");
prepare_db_files(&db);
assert!(
!db.exists(),
"no file may be created in a missing directory"
);
}
#[cfg(unix)]
#[test]
fn test_prepare_db_files_tightens_executable_file_to_0600() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("oo.db");
fs::write(&db, b"").unwrap();
set_mode(&db, 0o755);
prepare_db_files(&db);
assert_eq!(mode_of(&db), 0o600, "an owned 0755 file ends at 0600");
}