Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,13 @@ fn validate_identifier(id: &str, label: &str) -> Result<()> {
id
);
}
if id == "." || id.starts_with('.') || id.ends_with('.') {
anyhow::bail!(
"Invalid {}: '{}' must not be '.', start with '.', or end with '.'",
label,
id
);
}
if !id
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.')
Expand Down Expand Up @@ -1585,6 +1592,13 @@ mod tests {
assert!(validate_identifier("-agent", "id").is_err());
}

#[test]
fn test_validate_identifier_dot_edges() {
assert!(validate_identifier(".", "id").is_err());
assert!(validate_identifier(".agent", "id").is_err());
assert!(validate_identifier("agent.", "id").is_err());
}

#[test]
fn test_validate_identifier_special_chars() {
assert!(validate_identifier("foo@bar", "id").is_err());
Expand Down
55 changes: 46 additions & 9 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;

Expand Down Expand Up @@ -35,12 +35,11 @@ impl GritConfig {
match serde_json::from_str(&content) {
Ok(config) => Ok(config),
Err(e) => {
eprintln!(
"warning: {} is malformed ({}), using default config",
anyhow::bail!(
"{} is malformed ({}); refusing to fall back to a different backend",
path.display(),
e
);
Ok(Self::default())
}
}
} else {
Expand All @@ -51,7 +50,29 @@ impl GritConfig {
pub fn save(&self, grit_dir: &Path) -> Result<()> {
let path = grit_dir.join("config.json");
let content = serde_json::to_string_pretty(self)?;
std::fs::write(&path, content)?;

#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};

let mut file = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.mode(0o600)
.open(&path)
.with_context(|| format!("failed to open {} for writing", path.display()))?;
file.write_all(content.as_bytes())?;
file.sync_all()?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
}

#[cfg(not(unix))]
{
std::fs::write(&path, content)?;
}

Ok(())
}
}
Expand Down Expand Up @@ -93,13 +114,29 @@ mod tests {
}

#[test]
fn test_load_malformed_json() {
fn test_load_malformed_json_fails_closed() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("config.json");
std::fs::write(&path, "not valid json {{{").unwrap();
let config = GritConfig::load(tmp.path()).unwrap();
assert_eq!(config.backend, "local");
assert!(config.s3.is_none());
let err = GritConfig::load(tmp.path()).unwrap_err().to_string();
assert!(err.contains("malformed"));
assert!(err.contains("refusing to fall back"));
}

#[cfg(unix)]
#[test]
fn test_save_uses_owner_only_permissions() {
use std::os::unix::fs::PermissionsExt;

let tmp = TempDir::new().unwrap();
let config = GritConfig::default();
config.save(tmp.path()).unwrap();
let mode = std::fs::metadata(tmp.path().join("config.json"))
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o600);
}

#[test]
Expand Down
5 changes: 5 additions & 0 deletions src/room/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ impl NotificationServer {
}

let listener = UnixListener::bind(&self.socket_path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&self.socket_path, std::fs::Permissions::from_mode(0o600))?;
}
let watchers: Arc<Mutex<Vec<UnixStream>>> = Arc::new(Mutex::new(Vec::new()));

let watchers_ref = watchers.clone();
Expand Down
Loading