1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
26
27use std::path::Path;
28
29#[derive(Debug, Clone)]
31pub struct BlockMarkers {
32 pub begin: String,
34 pub end: String,
36}
37
38impl BlockMarkers {
39 pub fn standard(app_name: &str) -> Self {
41 Self {
42 begin: format!("# BEGIN {app_name} managed block -- do not edit"),
43 end: format!("# END {app_name} managed block"),
44 }
45 }
46
47 pub fn with_id(app_name: &str, id: &str) -> Self {
51 Self {
52 begin: format!("# --- BEGIN {app_name} managed ({id}) ---"),
53 end: format!("# --- END {app_name} managed ({id}) ---"),
54 }
55 }
56
57 pub fn custom(begin: impl Into<String>, end: impl Into<String>) -> Self {
59 Self {
60 begin: begin.into(),
61 end: end.into(),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum BlockInstallResult {
69 Installed,
71 Replaced,
73 AlreadyPresent,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum BlockRemoveResult {
80 Removed,
82 NotPresent,
84}
85
86pub fn find_block(content: &str, markers: &BlockMarkers) -> Option<(usize, usize)> {
91 let begin_idx = content.find(&markers.begin)?;
92 let after_begin = begin_idx + markers.begin.len();
93 let end_idx = content[after_begin..].find(&markers.end)?;
94 let absolute_end = after_begin + end_idx + markers.end.len();
95 let end_with_newline = if content[absolute_end..].starts_with('\n') {
97 absolute_end + 1
98 } else {
99 absolute_end
100 };
101 Some((begin_idx, end_with_newline))
102}
103
104pub fn has_block(content: &str, markers: &BlockMarkers) -> bool {
106 find_block(content, markers).is_some()
107}
108
109pub fn build_block(markers: &BlockMarkers, body: &str) -> String {
114 let mut block = String::new();
115 block.push_str(&markers.begin);
116 block.push('\n');
117 block.push_str(body);
118 if !body.ends_with('\n') {
119 block.push('\n');
120 }
121 block.push_str(&markers.end);
122 block
123}
124
125pub fn upsert_block(content: &str, markers: &BlockMarkers, block: &str) -> String {
130 if let Some((start, end)) = find_block(content, markers) {
131 let mut result = String::with_capacity(content.len());
133 result.push_str(&content[..start]);
134 result.push_str(block);
135 if !block.ends_with('\n') {
136 result.push('\n');
137 }
138 result.push_str(&content[end..]);
139 result
140 } else {
141 let mut result = content.to_string();
143 if !result.is_empty() && !result.ends_with('\n') {
144 result.push('\n');
145 }
146 if !result.is_empty() && !result.ends_with("\n\n") {
147 result.push('\n');
148 }
149 result.push_str(block);
150 if !block.ends_with('\n') {
151 result.push('\n');
152 }
153 result
154 }
155}
156
157pub fn remove_block(content: &str, markers: &BlockMarkers) -> (String, BlockRemoveResult) {
162 let Some((start, end)) = find_block(content, markers) else {
163 return (content.to_string(), BlockRemoveResult::NotPresent);
164 };
165
166 let mut result = String::with_capacity(content.len());
167 result.push_str(&content[..start]);
168 result.push_str(&content[end..]);
169
170 while result.contains("\n\n\n") {
172 result = result.replace("\n\n\n", "\n\n");
173 }
174
175 let trimmed = result.trim_end();
177 let mut final_result = trimmed.to_string();
178 if !final_result.is_empty() {
179 final_result.push('\n');
180 }
181
182 (final_result, BlockRemoveResult::Removed)
183}
184
185pub fn read_config_file(path: &Path) -> std::io::Result<Option<String>> {
189 match std::fs::read_to_string(path) {
190 Ok(content) => Ok(Some(content.replace("\r\n", "\n"))),
191 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
192 Err(e) => Err(e),
193 }
194}
195
196pub fn write_config_file(path: &Path, content: &str) -> std::io::Result<()> {
198 if let Some(parent) = path.parent() {
199 std::fs::create_dir_all(parent)?;
200 }
201 std::fs::write(path, content)?;
202 #[cfg(unix)]
203 {
204 use std::os::unix::fs::PermissionsExt;
205 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
206 }
207 Ok(())
208}
209
210pub fn install_block_in_file(
214 path: &Path,
215 markers: &BlockMarkers,
216 body: &str,
217) -> std::io::Result<BlockInstallResult> {
218 let content = read_config_file(path)?.unwrap_or_default();
219 let block = build_block(markers, body);
220
221 if let Some((start, end)) = find_block(&content, markers) {
222 let existing = &content[start..end];
223 let new_with_nl = if block.ends_with('\n') {
224 block.clone()
225 } else {
226 format!("{block}\n")
227 };
228 if existing == new_with_nl {
229 return Ok(BlockInstallResult::AlreadyPresent);
230 }
231 }
232
233 let result = upsert_block(&content, markers, &block);
234 write_config_file(path, &result)?;
235
236 if has_block(&content, markers) {
237 Ok(BlockInstallResult::Replaced)
238 } else {
239 Ok(BlockInstallResult::Installed)
240 }
241}
242
243pub fn remove_block_from_file(
247 path: &Path,
248 markers: &BlockMarkers,
249) -> std::io::Result<BlockRemoveResult> {
250 let Some(content) = read_config_file(path)? else {
251 return Ok(BlockRemoveResult::NotPresent);
252 };
253 let (result, status) = remove_block(&content, markers);
254 if status == BlockRemoveResult::Removed {
255 write_config_file(path, &result)?;
256 }
257 Ok(status)
258}
259
260#[cfg(test)]
261#[allow(clippy::unwrap_used, clippy::panic)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn standard_markers() {
267 let m = BlockMarkers::standard("sshenc");
268 assert_eq!(m.begin, "# BEGIN sshenc managed block -- do not edit");
269 assert_eq!(m.end, "# END sshenc managed block");
270 }
271
272 #[test]
273 fn markers_with_id() {
274 let m = BlockMarkers::with_id("awsenc", "production");
275 assert_eq!(m.begin, "# --- BEGIN awsenc managed (production) ---");
276 assert_eq!(m.end, "# --- END awsenc managed (production) ---");
277 }
278
279 #[test]
280 fn build_block_adds_markers() {
281 let m = BlockMarkers::standard("test");
282 let block = build_block(&m, "key = value\n");
283 assert_eq!(
284 block,
285 "# BEGIN test managed block -- do not edit\nkey = value\n# END test managed block"
286 );
287 }
288
289 #[test]
290 fn build_block_ensures_trailing_newline_on_body() {
291 let m = BlockMarkers::standard("test");
292 let block = build_block(&m, "key = value");
293 assert!(block.contains("key = value\n# END"));
294 }
295
296 #[test]
297 fn find_block_locates_markers() {
298 let m = BlockMarkers::standard("app");
299 let content = "before\n# BEGIN app managed block -- do not edit\nstuff\n# END app managed block\nafter\n";
300 let (start, end) = find_block(content, &m).unwrap();
301 assert_eq!(
302 &content[start..end],
303 "# BEGIN app managed block -- do not edit\nstuff\n# END app managed block\n"
304 );
305 }
306
307 #[test]
308 fn find_block_returns_none_when_missing() {
309 let m = BlockMarkers::standard("app");
310 assert!(find_block("no markers here", &m).is_none());
311 }
312
313 #[test]
314 fn find_block_returns_none_for_begin_without_end() {
315 let m = BlockMarkers::standard("app");
316 let content = "# BEGIN app managed block -- do not edit\nstuff\n";
317 assert!(find_block(content, &m).is_none());
318 }
319
320 #[test]
321 fn upsert_appends_to_empty() {
322 let m = BlockMarkers::standard("app");
323 let block = build_block(&m, "content\n");
324 let result = upsert_block("", &m, &block);
325 assert_eq!(result, format!("{block}\n"));
326 }
327
328 #[test]
329 fn upsert_appends_with_separator() {
330 let m = BlockMarkers::standard("app");
331 let block = build_block(&m, "content\n");
332 let result = upsert_block("existing\n", &m, &block);
333 assert!(result.starts_with("existing\n\n"));
334 assert!(result.contains("content\n"));
335 }
336
337 #[test]
338 fn upsert_replaces_existing() {
339 let m = BlockMarkers::standard("app");
340 let old = "before\n# BEGIN app managed block -- do not edit\nold\n# END app managed block\nafter\n";
341 let new_block = build_block(&m, "new content\n");
342 let result = upsert_block(old, &m, &new_block);
343 assert!(result.contains("new content"));
344 assert!(!result.contains("old"));
345 assert!(result.contains("before\n"));
346 assert!(result.contains("after\n"));
347 }
348
349 #[test]
350 fn remove_block_removes_and_cleans() {
351 let m = BlockMarkers::standard("app");
352 let content = "before\n\n# BEGIN app managed block -- do not edit\nstuff\n# END app managed block\n\nafter\n";
353 let (result, status) = remove_block(content, &m);
354 assert_eq!(status, BlockRemoveResult::Removed);
355 assert!(!result.contains("stuff"));
356 assert!(result.contains("before"));
357 assert!(result.contains("after"));
358 assert!(!result.contains("\n\n\n"));
359 }
360
361 #[test]
362 fn remove_block_not_present() {
363 let m = BlockMarkers::standard("app");
364 let (result, status) = remove_block("no block\n", &m);
365 assert_eq!(status, BlockRemoveResult::NotPresent);
366 assert_eq!(result, "no block\n");
367 }
368
369 #[test]
370 fn has_block_true_when_present() {
371 let m = BlockMarkers::standard("app");
372 let content = "# BEGIN app managed block -- do not edit\nx\n# END app managed block\n";
373 assert!(has_block(content, &m));
374 }
375
376 #[test]
377 fn has_block_false_when_absent() {
378 let m = BlockMarkers::standard("app");
379 assert!(!has_block("nothing here", &m));
380 }
381
382 #[test]
383 fn multiple_blocks_with_different_ids() {
384 let m1 = BlockMarkers::with_id("awsenc", "dev");
385 let m2 = BlockMarkers::with_id("awsenc", "prod");
386
387 let mut content = String::new();
388 let b1 = build_block(&m1, "dev config\n");
389 content = upsert_block(&content, &m1, &b1);
390 let b2 = build_block(&m2, "prod config\n");
391 content = upsert_block(&content, &m2, &b2);
392
393 assert!(has_block(&content, &m1));
394 assert!(has_block(&content, &m2));
395
396 let (content, _) = remove_block(&content, &m1);
397 assert!(!has_block(&content, &m1));
398 assert!(has_block(&content, &m2));
399 }
400
401 #[test]
402 fn upsert_preserves_content_around_block() {
403 let m = BlockMarkers::standard("app");
404 let existing = "[section1]\nkey1 = val1\n\n# BEGIN app managed block -- do not edit\nold\n# END app managed block\n\n[section2]\nkey2 = val2\n";
405 let new_block = build_block(&m, "new\n");
406 let result = upsert_block(existing, &m, &new_block);
407 assert!(result.contains("[section1]\nkey1 = val1"));
408 assert!(result.contains("[section2]\nkey2 = val2"));
409 assert!(result.contains("new\n"));
410 assert!(!result.contains("old"));
411 }
412
413 #[test]
414 fn read_config_file_normalizes_crlf() {
415 let dir = std::env::temp_dir().join(format!(
416 "enclaveapp-config-block-test-{}",
417 std::process::id()
418 ));
419 std::fs::create_dir_all(&dir).unwrap();
420 let path = dir.join("test.conf");
421 std::fs::write(&path, "line1\r\nline2\r\n").unwrap();
422 let content = read_config_file(&path).unwrap().unwrap();
423 assert_eq!(content, "line1\nline2\n");
424 std::fs::remove_dir_all(&dir).unwrap();
425 }
426
427 #[test]
428 fn read_config_file_returns_none_for_missing() {
429 let path = std::path::PathBuf::from("/nonexistent/path/to/file");
430 assert!(read_config_file(&path).unwrap().is_none());
431 }
432
433 #[test]
434 fn install_and_remove_file_round_trip() {
435 let dir = std::env::temp_dir().join(format!(
436 "enclaveapp-config-block-file-test-{}",
437 std::process::id()
438 ));
439 std::fs::create_dir_all(&dir).unwrap();
440 let path = dir.join("config");
441 std::fs::write(&path, "[existing]\nkey = value\n").unwrap();
442
443 let m = BlockMarkers::standard("test-app");
444 let result = install_block_in_file(&path, &m, "managed = true\n").unwrap();
445 assert_eq!(result, BlockInstallResult::Installed);
446
447 let content = std::fs::read_to_string(&path).unwrap();
448 assert!(content.contains("[existing]"));
449 assert!(content.contains("managed = true"));
450
451 let result = install_block_in_file(&path, &m, "managed = true\n").unwrap();
453 assert_eq!(result, BlockInstallResult::AlreadyPresent);
454
455 let result = install_block_in_file(&path, &m, "managed = updated\n").unwrap();
457 assert_eq!(result, BlockInstallResult::Replaced);
458
459 let result = remove_block_from_file(&path, &m).unwrap();
460 assert_eq!(result, BlockRemoveResult::Removed);
461
462 let content = std::fs::read_to_string(&path).unwrap();
463 assert!(content.contains("[existing]"));
464 assert!(!content.contains("managed"));
465
466 std::fs::remove_dir_all(&dir).unwrap();
467 }
468
469 #[test]
470 fn install_block_creates_file_if_missing() {
471 let dir = std::env::temp_dir().join(format!(
472 "enclaveapp-config-block-create-test-{}",
473 std::process::id()
474 ));
475 drop(std::fs::remove_dir_all(&dir));
476 let path = dir.join("subdir").join("new-config");
477
478 let m = BlockMarkers::standard("test");
479 let result = install_block_in_file(&path, &m, "content\n").unwrap();
480 assert_eq!(result, BlockInstallResult::Installed);
481 assert!(path.exists());
482
483 std::fs::remove_dir_all(&dir).unwrap();
484 }
485
486 #[test]
487 fn custom_markers_exact_strings() {
488 let m = BlockMarkers::custom("##START", "##END");
489 assert_eq!(m.begin, "##START");
490 assert_eq!(m.end, "##END");
491 }
492
493 #[test]
494 fn custom_markers_used_in_find_block() {
495 let m = BlockMarkers::custom("// MANAGED START", "// MANAGED END");
496 let content = "code before\n// MANAGED START\nmanaged code\n// MANAGED END\ncode after\n";
497 assert!(find_block(content, &m).is_some());
498 }
499
500 #[test]
501 fn custom_markers_used_in_build_and_upsert() {
502 let m = BlockMarkers::custom("/* managed-start */", "/* managed-end */");
503 let block = build_block(&m, "content = 42;\n");
504 assert!(block.starts_with("/* managed-start */"));
505 assert!(block.ends_with("/* managed-end */"));
506 let result = upsert_block("", &m, &block);
507 assert!(has_block(&result, &m));
508 }
509
510 #[test]
511 fn build_block_body_already_has_trailing_newline() {
512 let m = BlockMarkers::standard("app");
513 let block = build_block(&m, "body line\n");
514 assert!(!block.contains("\n\n# END"));
516 assert!(block.contains("body line\n# END"));
517 }
518
519 #[test]
520 fn build_block_empty_body() {
521 let m = BlockMarkers::standard("app");
522 let block = build_block(&m, "");
523 assert!(block.contains("# BEGIN app managed block -- do not edit\n\n# END"));
525 }
526
527 #[test]
528 fn find_block_no_trailing_newline_at_end_of_string() {
529 let m = BlockMarkers::standard("app");
530 let content = "# BEGIN app managed block -- do not edit\nstuff\n# END app managed block";
532 let result = find_block(content, &m);
533 assert!(result.is_some());
534 let (start, end) = result.unwrap();
535 assert_eq!(end, content.len());
537 assert_eq!(
538 &content[start..end],
539 "# BEGIN app managed block -- do not edit\nstuff\n# END app managed block"
540 );
541 }
542
543 #[test]
544 fn remove_block_at_start_of_content() {
545 let m = BlockMarkers::standard("app");
546 let content = "# BEGIN app managed block -- do not edit\nmanaged\n# END app managed block\n\nafter content\n";
547 let (result, status) = remove_block(content, &m);
548 assert_eq!(status, BlockRemoveResult::Removed);
549 assert!(!result.contains("managed"));
550 assert!(result.contains("after content"));
551 }
552
553 #[test]
554 fn remove_block_at_end_of_content() {
555 let m = BlockMarkers::standard("app");
556 let content = "before content\n\n# BEGIN app managed block -- do not edit\nmanaged\n# END app managed block\n";
557 let (result, status) = remove_block(content, &m);
558 assert_eq!(status, BlockRemoveResult::Removed);
559 assert!(!result.contains("managed"));
560 assert!(result.contains("before content"));
561 }
562
563 #[test]
564 fn remove_block_leaves_empty_string_when_only_content() {
565 let m = BlockMarkers::standard("app");
566 let content = "# BEGIN app managed block -- do not edit\nonly\n# END app managed block\n";
567 let (result, status) = remove_block(content, &m);
568 assert_eq!(status, BlockRemoveResult::Removed);
569 assert!(result.is_empty());
570 }
571
572 #[test]
573 fn upsert_content_already_ending_with_double_newline() {
574 let m = BlockMarkers::standard("app");
575 let block = build_block(&m, "new\n");
576 let result = upsert_block("existing\n\n", &m, &block);
578 assert!(!result.contains("\n\n\n"));
579 }
580
581 #[test]
582 fn remove_block_from_file_missing_file_is_not_present() {
583 let path = Path::new("/nonexistent/absolutely/missing.conf");
584 let m = BlockMarkers::standard("app");
585 let result = remove_block_from_file(path, &m).unwrap();
586 assert_eq!(result, BlockRemoveResult::NotPresent);
587 }
588
589 #[test]
590 fn read_config_file_empty_file_returns_some_empty_string() {
591 let dir = std::env::temp_dir().join(format!(
592 "enclaveapp-config-block-empty-test-{}",
593 std::process::id()
594 ));
595 std::fs::create_dir_all(&dir).unwrap();
596 let path = dir.join("empty.conf");
597 std::fs::write(&path, "").unwrap();
598 let content = read_config_file(&path).unwrap();
599 assert_eq!(content, Some(String::new()));
600 std::fs::remove_dir_all(&dir).unwrap();
601 }
602
603 #[test]
604 fn has_block_false_with_only_begin_no_end() {
605 let m = BlockMarkers::standard("app");
606 let content = "# BEGIN app managed block -- do not edit\nstuff but no end";
607 assert!(!has_block(content, &m));
608 }
609
610 #[test]
611 fn markers_with_id_blocks_distinguish_by_id() {
612 let m_foo = BlockMarkers::with_id("app", "foo");
614 let m_bar = BlockMarkers::with_id("app", "bar");
615 let content =
616 "# --- BEGIN app managed (foo) ---\ncontent\n# --- END app managed (foo) ---\n";
617 assert!(has_block(content, &m_foo));
618 assert!(!has_block(content, &m_bar));
619 }
620
621 #[cfg(unix)]
622 #[test]
623 fn write_config_file_sets_permissions() {
624 let dir = std::env::temp_dir().join(format!(
625 "enclaveapp-config-block-perms-test-{}",
626 std::process::id()
627 ));
628 std::fs::create_dir_all(&dir).unwrap();
629 let path = dir.join("restricted");
630 write_config_file(&path, "secret\n").unwrap();
631
632 use std::os::unix::fs::PermissionsExt;
633 let perms = std::fs::metadata(&path).unwrap().permissions();
634 assert_eq!(perms.mode() & 0o777, 0o600);
635
636 std::fs::remove_dir_all(&dir).unwrap();
637 }
638}