@@ -8305,6 +8305,53 @@ pub fn lxc_update_settings(container: &str, settings: &LxcSettingsUpdate) -> Res
83058305 })
83068306}
83078307
8308+ /// Join LXC config lines into a file body, guaranteeing exactly one trailing
8309+ /// newline. `Vec<_>::join("\n")` drops the terminating newline, so a config
8310+ /// written straight from joined lines is POSIX-malformed — and, worse, any
8311+ /// later `>> config` append would land on the same line as the final
8312+ /// directive. Every config rewrite goes through this so no writer can
8313+ /// reintroduce that (the autostart toggle did, 2026-07: it wrote
8314+ /// `lxc.start.auto = 1` with no trailing newline).
8315+ fn lxc_config_body<S: AsRef<str>>(lines: &[S]) -> String {
8316+ let mut body = lines.iter().map(|s| s.as_ref()).collect::<Vec<&str>>().join("\n");
8317+ if !body.ends_with('\n') {
8318+ body.push('\n');
8319+ }
8320+ body
8321+ }
8322+
8323+ /// Write an LXC container config from its line vector with a guaranteed
8324+ /// trailing newline. See `lxc_config_body`.
8325+ fn write_lxc_config<S: AsRef<str>>(path: &str, lines: &[S]) -> std::io::Result<()> {
8326+ std::fs::write(path, lxc_config_body(lines))
8327+ }
8328+
8329+ #[cfg(test)]
8330+ mod lxc_config_body_tests {
8331+ use super::lxc_config_body;
8332+
8333+ #[test]
8334+ fn always_ends_with_exactly_one_newline() {
8335+ // The autostart-toggle regression: joined lines dropped the trailing
8336+ // newline. The body must always end with exactly one.
8337+ let owned = vec![
8338+ "lxc.start.auto = 1".to_string(),
8339+ "lxc.start.delay = 5".to_string(),
8340+ ];
8341+ assert_eq!(lxc_config_body(&owned), "lxc.start.auto = 1\nlxc.start.delay = 5\n");
8342+
8343+ // Works with &str slices too (the mount-remove path uses Vec<&str>).
8344+ assert_eq!(lxc_config_body(&["a", "b"]), "a\nb\n");
8345+
8346+ // Idempotent: an already-terminated final line is not doubled.
8347+ assert_eq!(lxc_config_body(&["x\n"]), "x\n");
8348+
8349+ // Empty input still yields a newline-terminated (empty) body.
8350+ let empty: [&str; 0] = [];
8351+ assert_eq!(lxc_config_body(&empty), "\n");
8352+ }
8353+ }
8354+
83088355/// Update LXC container autostart specifically
83098356pub fn lxc_set_autostart(container: &str, enabled: bool) -> Result<String, String> {
83108357 if is_proxmox() {
@@ -8332,7 +8379,7 @@ pub fn lxc_set_autostart(container: &str, enabled: bool) -> Result<String, Strin
83328379 new_lines.push("lxc.start.delay = 5".to_string());
83338380 }
83348381
8335- std::fs::write (&path, new_lines.join("\n") ).map_err(|e| e.to_string())?;
8382+ write_lxc_config (&path, & new_lines).map_err(|e| e.to_string())?;
83368383 }
83378384 Ok(format!("Autostart set to {}", enabled))
83388385}
@@ -8359,7 +8406,7 @@ pub fn lxc_set_network_link(container: &str, link: &str) -> Result<String, Strin
83598406 new_lines.push(format!("lxc.net.0.link = {}", link));
83608407 }
83618408
8362- std::fs::write (&path, new_lines.join("\n") ).map_err(|e| e.to_string())?;
8409+ write_lxc_config (&path, & new_lines).map_err(|e| e.to_string())?;
83638410 Ok(format!("Network link set to {}", link))
83648411}
83658412
@@ -11740,7 +11787,7 @@ pub fn lxc_migrate_fixup(new_name: &str) {
1174011787 }
1174111788 line.to_string()
1174211789 }).collect();
11743- let _ = std::fs::write (&config_path, updated.join("\n") );
11790+ let _ = write_lxc_config (&config_path, & updated);
1174411791 }
1174511792 // Mark setup done so lxc_post_start_setup doesn't reassign a bridge IP
1174611793 // at first boot — the carried rootfs already holds the original network
@@ -11818,7 +11865,7 @@ pub fn lxc_clone_fixup_ip(new_name: &str) {
1181811865
1181911866 }
1182011867
11821- let _ = std::fs::write (&config_path, updated.join("\n") );
11868+ let _ = write_lxc_config (&config_path, & updated);
1182211869 }
1182311870
1182411871 // Write the setup_done marker so lxc_post_start_setup doesn't
@@ -11872,7 +11919,7 @@ pub fn lxc_ensure_network_config(name: &str) {
1187211919 for (i, line) in additions.iter().enumerate() {
1187311920 lines.insert(insert_pos + i, line.clone());
1187411921 }
11875- let _ = std::fs::write (&config_path, lines.join("\n") );
11922+ let _ = write_lxc_config (&config_path, & lines);
1187611923
1187711924}
1187811925
@@ -12522,8 +12569,7 @@ pub fn lxc_remove_mount(container: &str, host_path: &str) -> Result<String, Stri
1252212569 })
1252312570 .collect();
1252412571
12525- let new_config = filtered.join("\n");
12526- std::fs::write(&config_path, &new_config)
12572+ write_lxc_config(&config_path, &filtered)
1252712573 .map_err(|e| format!("Failed to write config: {}", e))?;
1252812574
1252912575
0 commit comments