From c2625ddf0009ae06c065d86fb11442b5ca0dc9dd Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 12 Nov 2026 09:19:18 -0800 Subject: [PATCH] seaweed-volume: read security.toml values as TOML strings (#12582) The line parser only trimmed quotes, so `key = "abc\n"` gave the Rust volume server the bytes `abc\n` with a literal backslash while the Go servers sign with a newline, and every JWT failed. Literal strings kept their quotes and a trailing comment stayed part of the value. Decode each value with the TOML parser; text it rejects keeps the old quote trimming. Co-authored-by: Claude Opus 5.5 --- seaweed-volume/src/config.rs | 44 +++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/seaweed-volume/src/config.rs b/seaweed-volume/src/config.rs index fdbb8574e5..58efcf8adb 100644 --- a/seaweed-volume/src/config.rs +++ b/seaweed-volume/src/config.rs @@ -1175,7 +1175,7 @@ fn parse_security_config_with_env(path: &str, env: EnvLookup<'_>) -> SecurityCon if let Some((key, value)) = trimmed.split_once('=') { let key = key.trim(); - let value = value.trim().trim_matches('"'); + let value: &str = &toml_value_text(value); match section { Section::JwtSigningRead => match key { "key" => cfg.jwt_read_signing_key = value.as_bytes().to_vec(), @@ -1265,6 +1265,20 @@ fn parse_security_config_with_env(path: &str, env: EnvLookup<'_>) -> SecurityCon cfg } +/// Reads the right-hand side of `key = value` the way TOML (and so Go) does: +/// escapes decoded, literal strings kept as written, a trailing comment +/// dropped. Text TOML rejects keeps the plain quote trimming. +fn toml_value_text(raw: &str) -> String { + match format!("v = {raw}").parse::() { + Ok(mut table) => match table.remove("v") { + Some(toml::Value::String(text)) => text, + Some(other) => other.to_string(), + None => String::new(), + }, + Err(_) => raw.trim().trim_matches('"').to_string(), + } +} + fn resolve_security_config_path(path: &str, env: EnvLookup<'_>) -> Option { if !path.is_empty() { return Some(PathBuf::from(path)); @@ -1868,6 +1882,34 @@ ui = true assert!(cfg.access_ui); } + #[test] + fn test_parse_security_config_reads_toml_strings() { + for (encoded, expected) in [ + (r#""plain""#, "plain"), + (r#""key\n""#, "key\n"), + (r#""quote\"slash\\tab\t""#, "quote\"slash\\tab\t"), + (r#""key""#, "key"), + ("'literal\\key'", "literal\\key"), + (r#""key" # comment"#, "key"), + (r#""YWJj==""#, "YWJj=="), + ("unquoted", "unquoted"), + ] { + let tmp = tempfile::NamedTempFile::new().unwrap(); + std::fs::write( + tmp.path(), + format!( + "[jwt.signing]\nkey = {encoded}\n\n[jwt.signing.read]\nkey = {encoded}\n\ + expires_after_seconds = 30 # seconds\n" + ), + ) + .unwrap(); + let cfg = parse_security_config_with_env(tmp.path().to_str().unwrap(), &|_| None); + assert_eq!(cfg.jwt_signing_key, expected.as_bytes(), "{encoded}"); + assert_eq!(cfg.jwt_read_signing_key, expected.as_bytes(), "{encoded}"); + assert_eq!(cfg.jwt_read_signing_expires, 30, "{encoded}"); + } + } + #[test] fn test_parse_security_config_discovers_current_directory_default() { let _guard = process_state_lock();