@@ -374,9 +374,16 @@ fn cmdConfigSetDefault(host: []const u8, port: u16, out: std.fs.File) void {
374374 };
375375 defer file .close ();
376376
377- var existing : [16384 ]u8 = undefined ;
378- const n = file .read (& existing ) catch 0 ;
379- const existing_content = existing [0.. n ];
377+ // Read the whole file — a fixed 16 KiB buffer silently truncated larger
378+ // configs (e.g. many favorites), corrupting the rewrite.
379+ const existing_content = file .readToEndAlloc (std .heap .c_allocator , 1024 * 1024 ) catch | err | {
380+ const err_f = std .fs .File .stderr ();
381+ err_f .writeAll ("✗ Failed to read config: " ) catch {};
382+ err_f .writeAll (@errorName (err )) catch {};
383+ err_f .writeAll ("\n " ) catch {};
384+ std .process .exit (1 );
385+ };
386+ defer std .heap .c_allocator .free (existing_content );
380387
381388 // Simple string replacement for default server
382389 // In a real implementation, you'd use a proper Nickel parser
@@ -422,9 +429,16 @@ fn cmdConfigAddFavorite(name: []const u8, host: []const u8, port: u16, out: std.
422429 };
423430 defer file .close ();
424431
425- var existing : [16384 ]u8 = undefined ;
426- const n = file .read (& existing ) catch 0 ;
427- const existing_content = existing [0.. n ];
432+ // Read the whole file — a fixed 16 KiB buffer silently truncated larger
433+ // configs (e.g. many favorites), corrupting the rewrite.
434+ const existing_content = file .readToEndAlloc (std .heap .c_allocator , 1024 * 1024 ) catch | err | {
435+ const err_f = std .fs .File .stderr ();
436+ err_f .writeAll ("✗ Failed to read config: " ) catch {};
437+ err_f .writeAll (@errorName (err )) catch {};
438+ err_f .writeAll ("\n " ) catch {};
439+ std .process .exit (1 );
440+ };
441+ defer std .heap .c_allocator .free (existing_content );
428442
429443 // Add favorite to the favorites list
430444
@@ -468,7 +482,15 @@ fn cmdConfigListFavorites(out: std.fs.File) void {
468482 out .writeAll ("Use `gsa config show` to view the full config.\n " ) catch {};
469483}
470484
471- /// Helper: Write config content with replaced default server to a file.
485+ /// Helper: rewrite the `host = ...` / `port = ...` lines of the generated
486+ /// user-config.ncl with a new default server.
487+ ///
488+ /// This is a deliberately narrow, structure-aware text patch — NOT a Nickel
489+ /// parser. It relies on the shape GSA itself emits (`gsa config init` from the
490+ /// bundled template): top-level `host = "..."` and `port = N` lines. On any
491+ /// unexpected shape (either marker missing) it writes the input back unchanged
492+ /// rather than risk corrupting a hand-edited file. A full Nickel editor is out
493+ /// of scope for the CLI. Round-tripped by the tests at the bottom of this file.
472494fn writeWithDefaultServer (existing : []const u8 , host : []const u8 , port : u16 , out_file : std.fs.File ) ! void {
473495 const host_start = std .mem .indexOf (u8 , existing , "host = " ) orelse {
474496 _ = try out_file .write (existing );
@@ -496,7 +518,10 @@ fn writeWithDefaultServer(existing: []const u8, host: []const u8, port: u16, out
496518 }
497519}
498520
499- /// Helper: Write config content with a new favorite appended to a file.
521+ /// Helper: append a favorite entry before the closing `]` of the favorites
522+ /// list. Same philosophy as writeWithDefaultServer — a structure-aware patch of
523+ /// GSA's own generated file, not a Nickel parser; on a missing `]` it writes the
524+ /// input back unchanged. Round-tripped by the tests at the bottom of this file.
500525fn writeWithFavorite (existing : []const u8 , name : []const u8 , host : []const u8 , port : u16 , out_file : std.fs.File ) ! void {
501526 const fav_end = std .mem .indexOf (u8 , existing , "]" ) orelse {
502527 _ = try out_file .write (existing );
@@ -629,9 +654,79 @@ fn printUsage(out: std.fs.File) void {
629654 \\ help Show this help
630655 \\
631656 \\ Environment:
632- \\ GSA_VERISIMDB_URL VeriSimDB endpoint (default: http://[::1] :8090)
657+ \\ GSA_VERISIMDB_URL VeriSimDB endpoint (default: http://localhost :8090)
633658 \\ GSA_PROFILES_DIR Profiles directory (default: ./profiles)
634659 \\
635660 \\
636661 ) catch {};
637662}
663+
664+ // ── Tests ────────────────────────────────────────────────────────────────────
665+
666+ test "writeWithDefaultServer replaces host/port and preserves the rest" {
667+ var tmp = std .testing .tmpDir (.{});
668+ defer tmp .cleanup ();
669+ const existing =
670+ \\{
671+ \\ default_server = {
672+ \\ host = "old.example.com",
673+ \\ port = 12345,
674+ \\ },
675+ \\ favorites = [],
676+ \\}
677+ ;
678+ {
679+ const f = try tmp .dir .createFile ("c.ncl" , .{});
680+ defer f .close ();
681+ try writeWithDefaultServer (existing , "new.host" , 25565 , f );
682+ }
683+ const rf = try tmp .dir .openFile ("c.ncl" , .{});
684+ defer rf .close ();
685+ const content = try rf .readToEndAlloc (std .testing .allocator , 1 << 16 );
686+ defer std .testing .allocator .free (content );
687+
688+ try std .testing .expect (std .mem .indexOf (u8 , content , "host = \" new.host\" " ) != null );
689+ try std .testing .expect (std .mem .indexOf (u8 , content , "port = 25565" ) != null );
690+ try std .testing .expect (std .mem .indexOf (u8 , content , "old.example.com" ) == null );
691+ try std .testing .expect (std .mem .indexOf (u8 , content , "favorites = []" ) != null );
692+ }
693+
694+ test "writeWithFavorite inserts an entry before the closing bracket" {
695+ var tmp = std .testing .tmpDir (.{});
696+ defer tmp .cleanup ();
697+ const existing =
698+ \\{
699+ \\ favorites = [
700+ \\ ],
701+ \\}
702+ ;
703+ {
704+ const f = try tmp .dir .createFile ("c.ncl" , .{});
705+ defer f .close ();
706+ try writeWithFavorite (existing , "myfav" , "10.0.0.1" , 27015 , f );
707+ }
708+ const rf = try tmp .dir .openFile ("c.ncl" , .{});
709+ defer rf .close ();
710+ const content = try rf .readToEndAlloc (std .testing .allocator , 1 << 16 );
711+ defer std .testing .allocator .free (content );
712+
713+ try std .testing .expect (std .mem .indexOf (u8 , content , "name = \" myfav\" " ) != null );
714+ try std .testing .expect (std .mem .indexOf (u8 , content , "host = \" 10.0.0.1\" " ) != null );
715+ try std .testing .expect (std .mem .indexOf (u8 , content , "port = 27015" ) != null );
716+ }
717+
718+ test "config helpers preserve unknown-shape input unchanged (no corruption)" {
719+ var tmp = std .testing .tmpDir (.{});
720+ defer tmp .cleanup ();
721+ const weird = "this is not the generated config at all\n " ;
722+ {
723+ const f = try tmp .dir .createFile ("c.ncl" , .{});
724+ defer f .close ();
725+ try writeWithDefaultServer (weird , "x" , 1 , f );
726+ }
727+ const rf = try tmp .dir .openFile ("c.ncl" , .{});
728+ defer rf .close ();
729+ const content = try rf .readToEndAlloc (std .testing .allocator , 1 << 16 );
730+ defer std .testing .allocator .free (content );
731+ try std .testing .expectEqualStrings (weird , content );
732+ }
0 commit comments