1use core::fmt;
26use core::str::FromStr;
27
28use crate::error::CoreError;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Scope {
33 Default,
35 Global,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub enum KeyHalf {
43 #[default]
47 Unspecified,
48 Public,
50 Private,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum EnvSegment {
58 Literal(String),
60 Placeholder,
62}
63
64impl fmt::Display for EnvSegment {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 EnvSegment::Literal(s) => f.write_str(s),
68 EnvSegment::Placeholder => f.write_str("${ENV}"),
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct Coordinate {
78 pub scope: Scope,
80 pub environment: EnvSegment,
82 pub component: String,
84 pub key: String,
86 pub half: KeyHalf,
89}
90
91impl Coordinate {
92 pub fn canonical_path(&self) -> Result<String, CoreError> {
102 match &self.environment {
103 EnvSegment::Literal(env) => Ok(format!("{}/{}/{}", env, self.component, self.key)),
104 EnvSegment::Placeholder => Err(CoreError::NotStorable(
105 "coordinate has an unresolved `${ENV}` placeholder".to_string(),
106 )),
107 }
108 }
109
110 pub fn with_env(&self, env: &str) -> Coordinate {
114 match &self.environment {
115 EnvSegment::Placeholder => Coordinate {
116 scope: self.scope,
117 environment: EnvSegment::Literal(env.to_string()),
118 component: self.component.clone(),
119 key: self.key.clone(),
120 half: self.half,
121 },
122 EnvSegment::Literal(_) => self.clone(),
123 }
124 }
125
126 pub fn storage_id(&self) -> Result<String, CoreError> {
131 Ok(blake3::hash(self.canonical_path()?.as_bytes())
132 .to_hex()
133 .to_string())
134 }
135}
136
137impl FromStr for Coordinate {
138 type Err = CoreError;
139
140 fn from_str(s: &str) -> Result<Self, Self::Err> {
141 let invalid = |msg: &str| CoreError::InvalidCoordinate(msg.to_string());
142
143 let rest = s
144 .strip_prefix("secret:")
145 .ok_or_else(|| invalid("must start with `secret:`"))?;
146
147 let (rest, half) = match rest.split_once('#') {
150 Some((before, "public")) => (before, KeyHalf::Public),
151 Some((before, "private")) => (before, KeyHalf::Private),
152 Some((_, other)) => {
153 return Err(invalid(&format!(
154 "unknown coordinate fragment `#{other}` (only `#public`/`#private` are valid)"
155 )));
156 }
157 None => (rest, KeyHalf::Unspecified),
158 };
159
160 let (scope, path) = match rest.strip_prefix("//") {
161 Some(authority_and_path) => {
162 let (authority, path) = authority_and_path
163 .split_once('/')
164 .ok_or_else(|| invalid("scope form requires `//<authority>/<path>`"))?;
165 if authority != "global" {
166 return Err(invalid("only `//global/` scope selector is supported"));
167 }
168 (Scope::Global, path)
169 }
170 None => (Scope::Default, rest),
171 };
172
173 let segments: Vec<&str> = path.split('/').collect();
174 if segments.len() != 3 {
175 return Err(invalid("coordinate must have exactly three segments"));
176 }
177 if segments.iter().any(|seg| seg.is_empty()) {
178 return Err(invalid("segments must be non-empty"));
179 }
180
181 let environment = parse_env_segment(segments[0])?;
182 let component = parse_plain_segment(segments[1], "component")?;
183 let key = parse_plain_segment(segments[2], "key")?;
184
185 Ok(Coordinate {
186 scope,
187 environment,
188 component,
189 key,
190 half,
191 })
192 }
193}
194
195fn parse_env_segment(seg: &str) -> Result<EnvSegment, CoreError> {
198 if seg == "${ENV}" {
199 return Ok(EnvSegment::Placeholder);
200 }
201 if seg.contains("${") {
202 return Err(CoreError::InvalidCoordinate(
203 "only `${ENV}` interpolation is allowed in the environment segment".to_string(),
204 ));
205 }
206 Ok(EnvSegment::Literal(seg.to_string()))
207}
208
209fn parse_plain_segment(seg: &str, what: &str) -> Result<String, CoreError> {
212 if seg.contains("${") {
213 return Err(CoreError::InvalidCoordinate(format!(
214 "interpolation is not allowed in the {what} segment"
215 )));
216 }
217 Ok(seg.to_string())
218}
219
220impl fmt::Display for Coordinate {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 match self.scope {
223 Scope::Default => write!(
224 f,
225 "secret:{}/{}/{}",
226 self.environment, self.component, self.key
227 )?,
228 Scope::Global => write!(
229 f,
230 "secret://global/{}/{}/{}",
231 self.environment, self.component, self.key
232 )?,
233 }
234 match self.half {
235 KeyHalf::Unspecified => Ok(()),
236 KeyHalf::Public => f.write_str("#public"),
237 KeyHalf::Private => f.write_str("#private"),
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use proptest::prelude::*;
246
247 #[test]
248 fn parses_literal_three_segments() {
249 let c: Coordinate = "secret:prod/db/password".parse().unwrap();
250 assert_eq!(c.scope, Scope::Default);
251 assert_eq!(c.environment, EnvSegment::Literal("prod".to_string()));
252 assert_eq!(c.component, "db");
253 assert_eq!(c.key, "password");
254 }
255
256 #[test]
257 fn parses_env_placeholder() {
258 let c: Coordinate = "secret:${ENV}/db/password".parse().unwrap();
259 assert_eq!(c.environment, EnvSegment::Placeholder);
260 }
261
262 #[test]
263 fn parses_global_scope_selector() {
264 let c: Coordinate = "secret://global/prod/db/password".parse().unwrap();
265 assert_eq!(c.scope, Scope::Global);
266 assert_eq!(c.environment, EnvSegment::Literal("prod".to_string()));
267 assert_eq!(c.key, "password");
268 }
269
270 #[test]
271 fn rejects_two_segments() {
272 assert!("secret:prod/db".parse::<Coordinate>().is_err());
273 }
274
275 #[test]
276 fn rejects_four_segments() {
277 assert!(
278 "secret:prod/db/password/extra"
279 .parse::<Coordinate>()
280 .is_err()
281 );
282 }
283
284 #[test]
285 fn rejects_missing_scheme() {
286 assert!("prod/db/password".parse::<Coordinate>().is_err());
287 }
288
289 #[test]
290 fn rejects_empty_segment() {
291 assert!("secret:prod//password".parse::<Coordinate>().is_err());
292 }
293
294 #[test]
295 fn rejects_non_env_interpolation() {
296 assert!("secret:${FOO}/db/password".parse::<Coordinate>().is_err());
297 assert!(
298 "secret:prod/${COMPONENT}/password"
299 .parse::<Coordinate>()
300 .is_err()
301 );
302 assert!("secret:prod/db/${KEY}".parse::<Coordinate>().is_err());
303 }
304
305 #[test]
306 fn storage_id_ignores_scope() {
307 let default: Coordinate = "secret:prod/db/password".parse().unwrap();
310 let global: Coordinate = "secret://global/prod/db/password".parse().unwrap();
311 assert_eq!(default.canonical_path().unwrap(), "prod/db/password");
312 assert_eq!(default.storage_id().unwrap(), global.storage_id().unwrap());
313 }
314
315 #[test]
316 fn storage_id_is_blake3_hex_of_path() {
317 let c: Coordinate = "secret:prod/db/password".parse().unwrap();
318 let expected = blake3::hash(b"prod/db/password").to_hex().to_string();
319 assert_eq!(c.storage_id().unwrap(), expected);
320 }
321
322 #[test]
323 fn with_env_substitutes_placeholder_only() {
324 let ph: Coordinate = "secret:${ENV}/db/password".parse().unwrap();
325 let resolved = ph.with_env("prod");
326 assert_eq!(
327 resolved.environment,
328 EnvSegment::Literal("prod".to_string())
329 );
330 assert_eq!(resolved.canonical_path().unwrap(), "prod/db/password");
331
332 let lit: Coordinate = "secret:dev/db/password".parse().unwrap();
334 assert_eq!(lit.with_env("prod"), lit);
335 }
336
337 #[test]
338 fn placeholder_is_not_storable() {
339 let c: Coordinate = "secret:${ENV}/db/password".parse().unwrap();
340 assert!(matches!(c.canonical_path(), Err(CoreError::NotStorable(_))));
341 assert!(matches!(c.storage_id(), Err(CoreError::NotStorable(_))));
342 }
343
344 #[test]
345 fn parses_keypair_half_selector() {
346 let pubc: Coordinate = "secret:dev/ssh/deploy#public".parse().unwrap();
347 assert_eq!(pubc.half, KeyHalf::Public);
348 assert_eq!(pubc.key, "deploy");
349 let privc: Coordinate = "secret:dev/ssh/deploy#private".parse().unwrap();
350 assert_eq!(privc.half, KeyHalf::Private);
351 let plain: Coordinate = "secret:dev/ssh/deploy".parse().unwrap();
353 assert_eq!(plain.half, KeyHalf::Unspecified);
354 }
355
356 #[test]
357 fn half_selector_does_not_change_storage_id() {
358 let plain: Coordinate = "secret:dev/ssh/deploy".parse().unwrap();
361 let pubc: Coordinate = "secret:dev/ssh/deploy#public".parse().unwrap();
362 let privc: Coordinate = "secret:dev/ssh/deploy#private".parse().unwrap();
363 assert_eq!(plain.storage_id().unwrap(), pubc.storage_id().unwrap());
364 assert_eq!(plain.storage_id().unwrap(), privc.storage_id().unwrap());
365 }
366
367 #[test]
368 fn half_selector_round_trips_through_display() {
369 for uri in [
370 "secret:dev/ssh/deploy#public",
371 "secret:dev/ssh/deploy#private",
372 "secret://global/dev/ssh/deploy#private",
373 ] {
374 let c: Coordinate = uri.parse().unwrap();
375 assert_eq!(c.to_string(), uri);
376 assert_eq!(c.to_string().parse::<Coordinate>().unwrap(), c);
377 }
378 }
379
380 #[test]
381 fn rejects_unknown_fragment() {
382 assert!("secret:dev/ssh/deploy#frag".parse::<Coordinate>().is_err());
383 assert!("secret:dev/a/b#".parse::<Coordinate>().is_err());
384 }
385
386 #[test]
387 fn rejects_unknown_scope_authority() {
388 assert!(
389 "secret://local/prod/db/password"
390 .parse::<Coordinate>()
391 .is_err()
392 );
393 }
394
395 #[test]
396 fn display_round_trips() {
397 for uri in [
398 "secret:prod/db/password",
399 "secret:${ENV}/db/password",
400 "secret://global/prod/db/password",
401 ] {
402 let c: Coordinate = uri.parse().unwrap();
403 assert_eq!(c.to_string(), uri);
404 assert_eq!(c.to_string().parse::<Coordinate>().unwrap(), c);
406 }
407 }
408
409 proptest! {
410 #[test]
413 fn parse_never_panics_and_round_trips(s in ".*") {
414 if let Ok(c) = s.parse::<Coordinate>() {
415 prop_assert_eq!(c.to_string().parse::<Coordinate>().unwrap(), c);
416 }
417 }
418
419 #[test]
421 fn well_formed_literals_parse(
422 env in "[a-z][a-z0-9_-]{0,12}",
423 comp in "[a-z][a-z0-9_-]{0,12}",
424 key in "[a-z][a-z0-9_-]{0,12}",
425 ) {
426 let uri = format!("secret:{env}/{comp}/{key}");
427 let c = uri.parse::<Coordinate>().unwrap();
428 prop_assert_eq!(c.environment, EnvSegment::Literal(env));
429 prop_assert_eq!(c.component, comp);
430 prop_assert_eq!(c.key, key);
431 }
432 }
433
434 fn near_miss_token() -> impl Strategy<Value = String> {
444 prop_oneof![
445 Just("secret:".to_string()),
446 Just("//".to_string()),
447 Just("global".to_string()),
448 Just("/".to_string()),
449 Just("#".to_string()),
450 Just("#public".to_string()),
451 Just("#private".to_string()),
452 Just("${ENV}".to_string()),
453 Just("${FOO}".to_string()),
454 Just("${".to_string()),
455 Just("\0".to_string()),
456 Just("\n".to_string()),
457 Just("é".to_string()),
458 "[a-z0-9_-]{0,6}",
459 ]
460 }
461
462 proptest! {
463 #[test]
467 fn near_miss_never_panics_and_round_trips(
468 toks in proptest::collection::vec(near_miss_token(), 0..8)
469 ) {
470 let s = toks.concat();
471 if let Ok(c) = s.parse::<Coordinate>() {
472 prop_assert_eq!(c.to_string().parse::<Coordinate>().unwrap(), c);
473 }
474 }
475
476 #[test]
481 fn interpolation_outside_env_segment_is_rejected(
482 env in "[a-z][a-z0-9_-]{0,8}",
483 comp in "[a-z][a-z0-9_-]{0,8}",
484 key in "[a-z][a-z0-9_-]{0,8}",
485 inject in prop_oneof![Just("${X}"), Just("${ENV}"), Just("${COMPONENT}")],
486 ) {
487 let poisoned_comp = format!("secret:{env}/{comp}{inject}/{key}");
489 prop_assert!(poisoned_comp.parse::<Coordinate>().is_err());
490 let poisoned_key = format!("secret:{env}/{comp}/{key}{inject}");
492 prop_assert!(poisoned_key.parse::<Coordinate>().is_err());
493 }
494
495 #[test]
499 fn placeholder_never_yields_storage_id(
500 comp in "[a-z][a-z0-9_-]{0,8}",
501 key in "[a-z][a-z0-9_-]{0,8}",
502 ) {
503 let c: Coordinate = format!("secret:${{ENV}}/{comp}/{key}").parse().unwrap();
504 prop_assert!(matches!(c.canonical_path(), Err(CoreError::NotStorable(_))));
505 prop_assert!(matches!(c.storage_id(), Err(CoreError::NotStorable(_))));
506 }
507 }
508}