1use std::env;
4use std::fs;
5use std::io::Write;
6use std::path::Path;
7
8use age::secrecy::SecretString;
9
10pub const ENV_MURK_KEY: &str = "MURK_KEY";
12pub const ENV_MURK_KEY_FILE: &str = "MURK_KEY_FILE";
14pub const ENV_MURK_VAULT: &str = "MURK_VAULT";
16
17const IMPORT_SKIP: &[&str] = &[ENV_MURK_KEY, ENV_MURK_KEY_FILE, ENV_MURK_VAULT];
19
20#[cfg(unix)]
22const SECRET_FILE_MODE: u32 = 0o600;
23
24#[cfg(unix)]
26const WORLD_READABLE_MASK: u32 = 0o077;
27
28pub fn resolve_key() -> Result<SecretString, String> {
32 if let Some(k) = env::var(ENV_MURK_KEY).ok().filter(|k| !k.is_empty()) {
33 return Ok(SecretString::from(k));
34 }
35 if let Ok(path) = env::var(ENV_MURK_KEY_FILE) {
36 return fs::read_to_string(&path)
37 .map(|contents| SecretString::from(contents.trim().to_string()))
38 .map_err(|e| format!("cannot read MURK_KEY_FILE ({path}): {e}"));
39 }
40 Err(
41 "MURK_KEY not set — run `murk init` to generate a key, or ask a recipient to authorize you"
42 .into(),
43 )
44}
45
46pub fn parse_env(contents: &str) -> Vec<(String, String)> {
49 let mut pairs = Vec::new();
50
51 for line in contents.lines() {
52 let line = line.trim();
53
54 if line.is_empty() || line.starts_with('#') {
55 continue;
56 }
57
58 let line = line.strip_prefix("export ").unwrap_or(line);
59
60 let Some((key, value)) = line.split_once('=') else {
61 continue;
62 };
63
64 let key = key.trim();
65 let value = value.trim();
66
67 let value = value
69 .strip_prefix('"')
70 .and_then(|v| v.strip_suffix('"'))
71 .or_else(|| value.strip_prefix('\'').and_then(|v| v.strip_suffix('\'')))
72 .unwrap_or(value);
73
74 if key.is_empty() || IMPORT_SKIP.contains(&key) {
75 continue;
76 }
77
78 pairs.push((key.into(), value.into()));
79 }
80
81 pairs
82}
83
84pub fn warn_env_permissions() {
86 #[cfg(unix)]
87 {
88 use std::os::unix::fs::PermissionsExt;
89 let env_path = Path::new(".env");
90 if env_path.exists()
91 && let Ok(meta) = fs::metadata(env_path)
92 {
93 let mode = meta.permissions().mode();
94 if mode & WORLD_READABLE_MASK != 0 {
95 eprintln!(
96 "\x1b[1;33mwarning:\x1b[0m .env is readable by others (mode {:o}). Run: \x1b[1mchmod 600 .env\x1b[0m",
97 mode & 0o777
98 );
99 }
100 }
101 }
102}
103
104pub fn read_key_from_dotenv() -> Option<String> {
109 let contents = fs::read_to_string(".env").ok()?;
110 for line in contents.lines() {
111 let trimmed = line.trim();
112 if let Some(key) = trimmed.strip_prefix("export MURK_KEY=") {
113 return Some(key.to_string());
114 }
115 if let Some(key) = trimmed.strip_prefix("MURK_KEY=") {
116 return Some(key.to_string());
117 }
118 }
119 None
120}
121
122pub fn dotenv_has_murk_key() -> bool {
124 let env_path = Path::new(".env");
125 if !env_path.exists() {
126 return false;
127 }
128 let contents = fs::read_to_string(env_path).unwrap_or_default();
129 contents
130 .lines()
131 .any(|l| l.starts_with("MURK_KEY=") || l.starts_with("export MURK_KEY="))
132}
133
134pub fn write_key_to_dotenv(secret_key: &str) -> Result<(), String> {
139 let env_path = Path::new(".env");
140
141 let existing = if env_path.exists() {
143 let contents = fs::read_to_string(env_path).map_err(|e| format!("reading .env: {e}"))?;
144 let filtered: Vec<&str> = contents
145 .lines()
146 .filter(|l| !l.starts_with("MURK_KEY=") && !l.starts_with("export MURK_KEY="))
147 .collect();
148 filtered.join("\n") + "\n"
149 } else {
150 String::new()
151 };
152
153 let full_content = format!("{existing}export MURK_KEY={secret_key}\n");
154
155 #[cfg(unix)]
157 {
158 use std::os::unix::fs::OpenOptionsExt;
159 let mut file = fs::OpenOptions::new()
160 .create(true)
161 .write(true)
162 .truncate(true)
163 .mode(SECRET_FILE_MODE)
164 .open(env_path)
165 .map_err(|e| format!("opening .env: {e}"))?;
166 file.write_all(full_content.as_bytes())
167 .map_err(|e| format!("writing .env: {e}"))?;
168 }
169
170 #[cfg(not(unix))]
171 {
172 fs::write(env_path, &full_content).map_err(|e| format!("writing .env: {e}"))?;
173 }
174
175 Ok(())
176}
177
178#[derive(Debug, PartialEq, Eq)]
180pub enum EnvrcStatus {
181 AlreadyPresent,
183 Appended,
185 Created,
187}
188
189pub fn write_envrc(vault_name: &str) -> Result<EnvrcStatus, String> {
194 let envrc = Path::new(".envrc");
195 let murk_line = format!("eval \"$(murk export --vault {vault_name})\"");
196
197 if envrc.exists() {
198 let contents = fs::read_to_string(envrc).map_err(|e| format!("reading .envrc: {e}"))?;
199 if contents.contains("murk export") {
200 return Ok(EnvrcStatus::AlreadyPresent);
201 }
202 let mut file = fs::OpenOptions::new()
203 .append(true)
204 .open(envrc)
205 .map_err(|e| format!("writing .envrc: {e}"))?;
206 writeln!(file, "\n{murk_line}").map_err(|e| format!("writing .envrc: {e}"))?;
207 Ok(EnvrcStatus::Appended)
208 } else {
209 fs::write(envrc, format!("{murk_line}\n")).map_err(|e| format!("writing .envrc: {e}"))?;
210 Ok(EnvrcStatus::Created)
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use std::sync::Mutex;
218
219 static ENV_LOCK: Mutex<()> = Mutex::new(());
222
223 static CWD_LOCK: Mutex<()> = Mutex::new(());
226
227 #[test]
228 fn parse_env_empty() {
229 assert!(parse_env("").is_empty());
230 }
231
232 #[test]
233 fn parse_env_comments_and_blanks() {
234 let input = "# comment\n\n # another\n";
235 assert!(parse_env(input).is_empty());
236 }
237
238 #[test]
239 fn parse_env_basic() {
240 let input = "FOO=bar\nBAZ=qux\n";
241 let pairs = parse_env(input);
242 assert_eq!(
243 pairs,
244 vec![("FOO".into(), "bar".into()), ("BAZ".into(), "qux".into())]
245 );
246 }
247
248 #[test]
249 fn parse_env_double_quotes() {
250 let pairs = parse_env("KEY=\"hello world\"\n");
251 assert_eq!(pairs, vec![("KEY".into(), "hello world".into())]);
252 }
253
254 #[test]
255 fn parse_env_single_quotes() {
256 let pairs = parse_env("KEY='hello world'\n");
257 assert_eq!(pairs, vec![("KEY".into(), "hello world".into())]);
258 }
259
260 #[test]
261 fn parse_env_export_prefix() {
262 let pairs = parse_env("export FOO=bar\n");
263 assert_eq!(pairs, vec![("FOO".into(), "bar".into())]);
264 }
265
266 #[test]
267 fn parse_env_skips_murk_keys() {
268 let input = "MURK_KEY=secret\nMURK_KEY_FILE=/path\nMURK_VAULT=.murk\nKEEP=yes\n";
269 let pairs = parse_env(input);
270 assert_eq!(pairs, vec![("KEEP".into(), "yes".into())]);
271 }
272
273 #[test]
274 fn parse_env_equals_in_value() {
275 let pairs = parse_env("URL=postgres://host?opt=1\n");
276 assert_eq!(pairs, vec![("URL".into(), "postgres://host?opt=1".into())]);
277 }
278
279 #[test]
280 fn parse_env_no_equals_skipped() {
281 let pairs = parse_env("not-a-valid-line\nKEY=val\n");
282 assert_eq!(pairs, vec![("KEY".into(), "val".into())]);
283 }
284
285 #[test]
288 fn parse_env_empty_value() {
289 let pairs = parse_env("KEY=\n");
290 assert_eq!(pairs, vec![("KEY".into(), String::new())]);
291 }
292
293 #[test]
294 fn parse_env_trailing_whitespace() {
295 let pairs = parse_env("KEY=value \n");
296 assert_eq!(pairs, vec![("KEY".into(), "value".into())]);
297 }
298
299 #[test]
300 fn parse_env_unicode_value() {
301 let pairs = parse_env("KEY=hello🔐world\n");
302 assert_eq!(pairs, vec![("KEY".into(), "hello🔐world".into())]);
303 }
304
305 #[test]
306 fn parse_env_empty_key_skipped() {
307 let pairs = parse_env("=value\n");
308 assert!(pairs.is_empty());
309 }
310
311 #[test]
312 fn parse_env_mixed_quotes_unmatched() {
313 let pairs = parse_env("KEY=\"hello'\n");
315 assert_eq!(pairs, vec![("KEY".into(), "\"hello'".into())]);
316 }
317
318 #[test]
319 fn parse_env_multiple_murk_vars() {
320 let input = "MURK_KEY=x\nMURK_KEY_FILE=y\nMURK_VAULT=z\nA=1\nB=2\n";
322 let pairs = parse_env(input);
323 assert_eq!(
324 pairs,
325 vec![("A".into(), "1".into()), ("B".into(), "2".into())]
326 );
327 }
328
329 #[test]
330 fn resolve_key_from_env() {
331 let _lock = ENV_LOCK.lock().unwrap();
332 let key = "AGE-SECRET-KEY-1TEST";
333 unsafe { env::set_var("MURK_KEY", key) };
334 let result = resolve_key();
335 unsafe { env::remove_var("MURK_KEY") };
336
337 let secret = result.unwrap();
338 use age::secrecy::ExposeSecret;
339 assert_eq!(secret.expose_secret(), key);
340 }
341
342 #[test]
343 fn resolve_key_from_file() {
344 let _lock = ENV_LOCK.lock().unwrap();
345 unsafe { env::remove_var("MURK_KEY") };
346
347 let path = std::env::temp_dir().join("murk_test_key_file");
348 std::fs::write(&path, "AGE-SECRET-KEY-1FROMFILE\n").unwrap();
349
350 unsafe { env::set_var("MURK_KEY_FILE", path.to_str().unwrap()) };
351 let result = resolve_key();
352 unsafe { env::remove_var("MURK_KEY_FILE") };
353 std::fs::remove_file(&path).ok();
354
355 let secret = result.unwrap();
356 use age::secrecy::ExposeSecret;
357 assert_eq!(secret.expose_secret(), "AGE-SECRET-KEY-1FROMFILE");
358 }
359
360 #[test]
361 fn resolve_key_file_not_found() {
362 let _lock = ENV_LOCK.lock().unwrap();
363 unsafe { env::remove_var("MURK_KEY") };
364 unsafe { env::set_var("MURK_KEY_FILE", "/nonexistent/path/murk_key") };
365 let result = resolve_key();
366 unsafe { env::remove_var("MURK_KEY_FILE") };
367
368 assert!(result.is_err());
369 assert!(result.unwrap_err().contains("cannot read MURK_KEY_FILE"));
370 }
371
372 #[test]
373 fn resolve_key_neither_set() {
374 let _lock = ENV_LOCK.lock().unwrap();
375 unsafe { env::remove_var("MURK_KEY") };
376 unsafe { env::remove_var("MURK_KEY_FILE") };
377 let result = resolve_key();
378
379 assert!(result.is_err());
380 assert!(result.unwrap_err().contains("MURK_KEY not set"));
381 }
382
383 #[test]
384 fn resolve_key_empty_string_treated_as_unset() {
385 let _lock = ENV_LOCK.lock().unwrap();
386 unsafe { env::set_var("MURK_KEY", "") };
387 unsafe { env::remove_var("MURK_KEY_FILE") };
388 let result = resolve_key();
389 unsafe { env::remove_var("MURK_KEY") };
390
391 assert!(result.is_err());
392 assert!(result.unwrap_err().contains("MURK_KEY not set"));
393 }
394
395 #[test]
396 fn resolve_key_murk_key_takes_priority_over_file() {
397 let _lock = ENV_LOCK.lock().unwrap();
398 let direct_key = "AGE-SECRET-KEY-1DIRECT";
399 let file_key = "AGE-SECRET-KEY-1FILE";
400
401 let path = std::env::temp_dir().join("murk_test_key_priority");
402 std::fs::write(&path, format!("{file_key}\n")).unwrap();
403
404 unsafe { env::set_var("MURK_KEY", direct_key) };
405 unsafe { env::set_var("MURK_KEY_FILE", path.to_str().unwrap()) };
406 let result = resolve_key();
407 unsafe { env::remove_var("MURK_KEY") };
408 unsafe { env::remove_var("MURK_KEY_FILE") };
409 std::fs::remove_file(&path).ok();
410
411 let secret = result.unwrap();
412 use age::secrecy::ExposeSecret;
413 assert_eq!(secret.expose_secret(), direct_key);
414 }
415
416 #[cfg(unix)]
417 #[test]
418 fn warn_env_permissions_no_warning_on_secure_file() {
419 let _cwd = CWD_LOCK.lock().unwrap();
420 use std::os::unix::fs::PermissionsExt;
421
422 let dir = std::env::temp_dir().join("murk_test_perms");
423 let _ = std::fs::remove_dir_all(&dir);
424 std::fs::create_dir_all(&dir).unwrap();
425 let env_path = dir.join(".env");
426 std::fs::write(&env_path, "KEY=val\n").unwrap();
427 std::fs::set_permissions(&env_path, std::fs::Permissions::from_mode(0o600)).unwrap();
428
429 let original_dir = std::env::current_dir().unwrap();
431 std::env::set_current_dir(&dir).unwrap();
432 warn_env_permissions();
433 std::env::set_current_dir(original_dir).unwrap();
434
435 std::fs::remove_dir_all(&dir).unwrap();
436 }
437
438 #[test]
439 fn read_key_from_dotenv_export_form() {
440 let _cwd = CWD_LOCK.lock().unwrap();
441 let dir = std::env::temp_dir().join("murk_test_read_dotenv_export");
442 let _ = std::fs::remove_dir_all(&dir);
443 std::fs::create_dir_all(&dir).unwrap();
444 let env_path = dir.join(".env");
445 std::fs::write(&env_path, "export MURK_KEY=AGE-SECRET-KEY-1ABC\n").unwrap();
446
447 let original_dir = std::env::current_dir().unwrap();
448 std::env::set_current_dir(&dir).unwrap();
449 let result = read_key_from_dotenv();
450 std::env::set_current_dir(original_dir).unwrap();
451
452 assert_eq!(result, Some("AGE-SECRET-KEY-1ABC".into()));
453 std::fs::remove_dir_all(&dir).unwrap();
454 }
455
456 #[test]
457 fn read_key_from_dotenv_bare_form() {
458 let _cwd = CWD_LOCK.lock().unwrap();
459 let dir = std::env::temp_dir().join("murk_test_read_dotenv_bare");
460 let _ = std::fs::remove_dir_all(&dir);
461 std::fs::create_dir_all(&dir).unwrap();
462 let env_path = dir.join(".env");
463 std::fs::write(&env_path, "MURK_KEY=AGE-SECRET-KEY-1XYZ\n").unwrap();
464
465 let original_dir = std::env::current_dir().unwrap();
466 std::env::set_current_dir(&dir).unwrap();
467 let result = read_key_from_dotenv();
468 std::env::set_current_dir(original_dir).unwrap();
469
470 assert_eq!(result, Some("AGE-SECRET-KEY-1XYZ".into()));
471 std::fs::remove_dir_all(&dir).unwrap();
472 }
473
474 #[test]
475 fn read_key_from_dotenv_missing_file() {
476 let _cwd = CWD_LOCK.lock().unwrap();
477 let dir = std::env::temp_dir().join("murk_test_read_dotenv_missing");
478 let _ = std::fs::remove_dir_all(&dir);
479 std::fs::create_dir_all(&dir).unwrap();
480
481 let original_dir = std::env::current_dir().unwrap();
482 std::env::set_current_dir(&dir).unwrap();
483 let result = read_key_from_dotenv();
484 std::env::set_current_dir(original_dir).unwrap();
485
486 assert_eq!(result, None);
487 std::fs::remove_dir_all(&dir).unwrap();
488 }
489
490 #[test]
491 fn dotenv_has_murk_key_true() {
492 let _cwd = CWD_LOCK.lock().unwrap();
493 let dir = std::env::temp_dir().join("murk_test_has_key_true");
494 let _ = std::fs::remove_dir_all(&dir);
495 std::fs::create_dir_all(&dir).unwrap();
496 std::fs::write(dir.join(".env"), "MURK_KEY=test\n").unwrap();
497
498 let original_dir = std::env::current_dir().unwrap();
499 std::env::set_current_dir(&dir).unwrap();
500 assert!(dotenv_has_murk_key());
501 std::env::set_current_dir(original_dir).unwrap();
502
503 std::fs::remove_dir_all(&dir).unwrap();
504 }
505
506 #[test]
507 fn dotenv_has_murk_key_false() {
508 let _cwd = CWD_LOCK.lock().unwrap();
509 let dir = std::env::temp_dir().join("murk_test_has_key_false");
510 let _ = std::fs::remove_dir_all(&dir);
511 std::fs::create_dir_all(&dir).unwrap();
512 std::fs::write(dir.join(".env"), "OTHER=val\n").unwrap();
513
514 let original_dir = std::env::current_dir().unwrap();
515 std::env::set_current_dir(&dir).unwrap();
516 assert!(!dotenv_has_murk_key());
517 std::env::set_current_dir(original_dir).unwrap();
518
519 std::fs::remove_dir_all(&dir).unwrap();
520 }
521
522 #[test]
523 fn dotenv_has_murk_key_no_file() {
524 let _cwd = CWD_LOCK.lock().unwrap();
525 let dir = std::env::temp_dir().join("murk_test_has_key_nofile");
526 let _ = std::fs::remove_dir_all(&dir);
527 std::fs::create_dir_all(&dir).unwrap();
528
529 let original_dir = std::env::current_dir().unwrap();
530 std::env::set_current_dir(&dir).unwrap();
531 assert!(!dotenv_has_murk_key());
532 std::env::set_current_dir(original_dir).unwrap();
533
534 std::fs::remove_dir_all(&dir).unwrap();
535 }
536
537 #[test]
538 fn write_key_to_dotenv_creates_new() {
539 let _cwd = CWD_LOCK.lock().unwrap();
540 let dir = std::env::temp_dir().join("murk_test_write_key_new");
541 let _ = std::fs::remove_dir_all(&dir);
542 std::fs::create_dir_all(&dir).unwrap();
543
544 let original_dir = std::env::current_dir().unwrap();
545 std::env::set_current_dir(&dir).unwrap();
546 write_key_to_dotenv("AGE-SECRET-KEY-1NEW").unwrap();
547
548 let contents = std::fs::read_to_string(dir.join(".env")).unwrap();
549 assert!(contents.contains("export MURK_KEY=AGE-SECRET-KEY-1NEW"));
550
551 std::env::set_current_dir(original_dir).unwrap();
552 std::fs::remove_dir_all(&dir).unwrap();
553 }
554
555 #[test]
556 fn write_key_to_dotenv_replaces_existing() {
557 let _cwd = CWD_LOCK.lock().unwrap();
558 let dir = std::env::temp_dir().join("murk_test_write_key_replace");
559 let _ = std::fs::remove_dir_all(&dir);
560 std::fs::create_dir_all(&dir).unwrap();
561 std::fs::write(
562 dir.join(".env"),
563 "OTHER=keep\nMURK_KEY=old\nexport MURK_KEY=also_old\n",
564 )
565 .unwrap();
566
567 let original_dir = std::env::current_dir().unwrap();
568 std::env::set_current_dir(&dir).unwrap();
569 write_key_to_dotenv("AGE-SECRET-KEY-1REPLACED").unwrap();
570
571 let contents = std::fs::read_to_string(dir.join(".env")).unwrap();
572 assert!(contents.contains("OTHER=keep"));
573 assert!(contents.contains("export MURK_KEY=AGE-SECRET-KEY-1REPLACED"));
574 assert!(!contents.contains("MURK_KEY=old"));
575 assert!(!contents.contains("also_old"));
576
577 std::env::set_current_dir(original_dir).unwrap();
578 std::fs::remove_dir_all(&dir).unwrap();
579 }
580
581 #[cfg(unix)]
582 #[test]
583 fn write_key_to_dotenv_permissions_are_600() {
584 let _cwd = CWD_LOCK.lock().unwrap();
585 use std::os::unix::fs::PermissionsExt;
586
587 let dir = std::env::temp_dir().join("murk_test_write_key_perms");
588 let _ = std::fs::remove_dir_all(&dir);
589 std::fs::create_dir_all(&dir).unwrap();
590
591 let original_dir = std::env::current_dir().unwrap();
592 std::env::set_current_dir(&dir).unwrap();
593
594 write_key_to_dotenv("AGE-SECRET-KEY-1PERMTEST").unwrap();
596 let meta = std::fs::metadata(dir.join(".env")).unwrap();
597 assert_eq!(
598 meta.permissions().mode() & 0o777,
599 SECRET_FILE_MODE,
600 "new .env should be created with mode 600"
601 );
602
603 write_key_to_dotenv("AGE-SECRET-KEY-1PERMTEST2").unwrap();
605 let meta = std::fs::metadata(dir.join(".env")).unwrap();
606 assert_eq!(
607 meta.permissions().mode() & 0o777,
608 SECRET_FILE_MODE,
609 "rewritten .env should maintain mode 600"
610 );
611
612 std::env::set_current_dir(original_dir).unwrap();
613 std::fs::remove_dir_all(&dir).unwrap();
614 }
615
616 #[test]
617 fn write_envrc_creates_new() {
618 let _cwd = CWD_LOCK.lock().unwrap();
619 let dir = std::env::temp_dir().join("murk_test_envrc_new");
620 let _ = std::fs::remove_dir_all(&dir);
621 std::fs::create_dir_all(&dir).unwrap();
622
623 let original_dir = std::env::current_dir().unwrap();
624 std::env::set_current_dir(&dir).unwrap();
625 let status = write_envrc(".murk").unwrap();
626 assert_eq!(status, EnvrcStatus::Created);
627
628 let contents = std::fs::read_to_string(dir.join(".envrc")).unwrap();
629 assert!(contents.contains("murk export --vault .murk"));
630
631 std::env::set_current_dir(original_dir).unwrap();
632 std::fs::remove_dir_all(&dir).unwrap();
633 }
634
635 #[test]
636 fn write_envrc_appends() {
637 let _cwd = CWD_LOCK.lock().unwrap();
638 let dir = std::env::temp_dir().join("murk_test_envrc_append");
639 let _ = std::fs::remove_dir_all(&dir);
640 std::fs::create_dir_all(&dir).unwrap();
641 std::fs::write(dir.join(".envrc"), "existing content\n").unwrap();
642
643 let original_dir = std::env::current_dir().unwrap();
644 std::env::set_current_dir(&dir).unwrap();
645 let status = write_envrc(".murk").unwrap();
646 assert_eq!(status, EnvrcStatus::Appended);
647
648 let contents = std::fs::read_to_string(dir.join(".envrc")).unwrap();
649 assert!(contents.contains("existing content"));
650 assert!(contents.contains("murk export"));
651
652 std::env::set_current_dir(original_dir).unwrap();
653 std::fs::remove_dir_all(&dir).unwrap();
654 }
655
656 #[test]
657 fn write_envrc_already_present() {
658 let _cwd = CWD_LOCK.lock().unwrap();
659 let dir = std::env::temp_dir().join("murk_test_envrc_present");
660 let _ = std::fs::remove_dir_all(&dir);
661 std::fs::create_dir_all(&dir).unwrap();
662 std::fs::write(
663 dir.join(".envrc"),
664 "eval \"$(murk export --vault .murk)\"\n",
665 )
666 .unwrap();
667
668 let original_dir = std::env::current_dir().unwrap();
669 std::env::set_current_dir(&dir).unwrap();
670 let status = write_envrc(".murk").unwrap();
671 assert_eq!(status, EnvrcStatus::AlreadyPresent);
672
673 std::env::set_current_dir(original_dir).unwrap();
674 std::fs::remove_dir_all(&dir).unwrap();
675 }
676}