@@ -337,7 +337,11 @@ const TestingSystem = struct {
337337 if (std .mem .eql (u8 , key , kv .key )) return kv .value ;
338338 }
339339 system .deinit ();
340- std .debug .panic ("the result of `getenv(\" {}\" )` must explicitly specified in the TestingSystem" , .{std .zig .fmtEscapes (key )});
340+ if (@hasDecl (std .zig , "fmtString" )) {
341+ std .debug .panic ("the result of `getenv(\" {f}\" )` must explicitly specified in the TestingSystem" , .{std .zig .fmtString (key )});
342+ } else {
343+ std .debug .panic ("the result of `getenv(\" {}\" )` must explicitly specified in the TestingSystem" , .{std .zig .fmtEscapes (key )});
344+ }
341345 }
342346
343347 /// Asserts that the file is specified in `TestingSystem.files`.
@@ -349,7 +353,11 @@ const TestingSystem = struct {
349353 if (std .mem .eql (u8 , file_path , kv .path )) break kv ;
350354 } else {
351355 system .deinit ();
352- std .debug .panic ("`openFile(\" {0}\" , \" {1}\" )` has been called on an unexpected file" , .{ std .zig .fmtEscapes (dir_path ), std .zig .fmtEscapes (sub_path ) });
356+ if (@hasDecl (std .zig , "fmtString" )) {
357+ std .debug .panic ("`openFile(\" {0f}\" , \" {1f}\" )` has been called on an unexpected file" , .{ std .zig .fmtString (dir_path ), std .zig .fmtString (sub_path ) });
358+ } else {
359+ std .debug .panic ("`openFile(\" {0}\" , \" {1}\" )` has been called on an unexpected file" , .{ std .zig .fmtEscapes (dir_path ), std .zig .fmtEscapes (sub_path ) });
360+ }
353361 };
354362
355363 const data = kv .data orelse return error .FileNotFound ;
@@ -435,40 +443,12 @@ fn xdgUserDirLookup(
435443 try system .openFile (home_dir , ".config/user-dirs.dirs" );
436444 defer file .close ();
437445
438- var fbr = std . io . bufferedReaderSize ( 512 , file . reader ()) ;
439- const reader = fbr . reader ( );
446+ var buffer : [ xdg_user_dir_lookup_line_buffer_size + 1 ] u8 = undefined ;
447+ var line_it : if ( @hasDecl ( std.fs.File , "stdin" )) LineIterator else OldLineIterator = . init ( file );
440448
441449 var user_dir : ? []u8 = null ;
442- outer : while (true ) {
443- var buffer : [xdg_user_dir_lookup_line_buffer_size + 1 ]u8 = undefined ;
444-
445- // Similar to `readUntilDelimiterOrEof` but also writes a null-terminator
446- var line : [:0 ]u8 = for (& buffer , 0.. ) | * out , index | {
447- const byte = reader .readByte () catch | err | switch (err ) {
448- error .EndOfStream = > if (index == 0 ) break :outer else '\n ' ,
449- else = > | e | return e ,
450- };
451- if (byte == '\n ' ) {
452- out .* = 0 ;
453- break buffer [0.. index :0 ];
454- }
455- out .* = byte ;
456- } else blk : {
457- // This happens when the line is longer than 511 characters
458- // There are four possible ways to handle this:
459- // - use dynamic allocation to acquire enough storage
460- // - return an error
461- // - skip the line
462- // - truncate the line
463- //
464- // The xdg-user-dir implementation chooses to trunacte the line.
465- // See "getPath - user-dirs.dirs - very long line" test
466-
467- try reader .skipUntilDelimiterOrEof ('\n ' );
468-
469- buffer [buffer .len - 1 ] = 0 ;
470- break :blk buffer [0 .. buffer .len - 1 :0 ];
471- };
450+ while (try line_it .next (& buffer )) | line_capture | {
451+ var line = line_capture ;
472452
473453 while (line [0 ] == ' ' or line [0 ] == '\t ' )
474454 line = line [1.. ];
@@ -549,6 +529,108 @@ fn xdgUserDirLookup(
549529 return user_dir ;
550530}
551531
532+ const OldLineIterator = struct {
533+ fbr : std .io .BufferedReader (4096 , std .fs .File .Reader ),
534+
535+ fn init (file : std.fs.File ) OldLineIterator {
536+ return .{
537+ .fbr = std .io .bufferedReader (file .reader ()),
538+ };
539+ }
540+
541+ fn next (it : * OldLineIterator , buffer : []u8 ) std.posix.ReadError ! ? [:0 ]const u8 {
542+ const reader = it .fbr .reader ();
543+
544+ // Similar to `readUntilDelimiterOrEof` but also writes a null-terminator
545+ for (buffer , 0.. ) | * out , index | {
546+ const byte = reader .readByte () catch | err | switch (err ) {
547+ error .EndOfStream = > if (index == 0 ) return null else '\n ' ,
548+ else = > | e | return e ,
549+ };
550+ if (byte == '\n ' ) {
551+ out .* = 0 ;
552+ return buffer [0.. index :0 ];
553+ }
554+ out .* = byte ;
555+ } else {
556+ // This happens when the line is longer than 511 characters
557+ // There are four possible ways to handle this:
558+ // - use dynamic allocation to acquire enough storage
559+ // - return an error
560+ // - skip the line
561+ // - truncate the line
562+ //
563+ // The xdg-user-dir implementation chooses to trunacte the line.
564+ // See "getPath - user-dirs.dirs - very long line" test
565+
566+ try reader .skipUntilDelimiterOrEof ('\n ' );
567+
568+ buffer [buffer .len - 1 ] = 0 ;
569+ return buffer [0 .. buffer .len - 1 :0 ];
570+ }
571+ }
572+ };
573+
574+ const LineIterator = struct {
575+ file_reader : std.fs.File.Reader ,
576+ keep_reading : bool ,
577+ discard_until_newline : bool ,
578+
579+ fn init (file : std.fs.File ) LineIterator {
580+ return .{
581+ .file_reader = file .reader (undefined ),
582+ .keep_reading = true ,
583+ .discard_until_newline = false ,
584+ };
585+ }
586+
587+ fn next (it : * LineIterator , buffer : []u8 ) std.posix.ReadError ! ? [:'\n ' ]const u8 {
588+ if (! it .keep_reading ) return null ;
589+
590+ const reader = & it .file_reader .interface ;
591+ reader .buffer = buffer [0 .. buffer .len - 1 ]; // leave space for the sentinel
592+
593+ if (it .discard_until_newline ) {
594+ it .discard_until_newline = false ;
595+ _ = reader .discardDelimiterInclusive ('\n ' ) catch | discard_err | switch (discard_err ) {
596+ error .ReadFailed = > return it .file_reader .err .? ,
597+ error .EndOfStream = > return null ,
598+ };
599+ }
600+
601+ return reader .takeSentinel ('\n ' ) catch | err | switch (err ) {
602+ error .ReadFailed = > return it .file_reader .err .? ,
603+ error .EndOfStream = > {
604+ if (reader .bufferedLen () == 0 )
605+ return null ; // trailing newline
606+
607+ // This is the last line
608+ buffer [reader .end ] = '\n ' ;
609+ const line = buffer [reader .seek .. reader .end :'\n ' ];
610+ it .keep_reading = false ;
611+ return line ;
612+ },
613+ error .StreamTooLong = > {
614+ // This happens when the line is longer than 511 characters
615+ // There are four possible ways to handle this:
616+ // - use dynamic allocation to acquire enough storage
617+ // - return an error
618+ // - skip the line
619+ // - truncate the line
620+ //
621+ // The xdg-user-dir implementation chooses to trunacte the line.
622+ // See "getPath - user-dirs.dirs - very long line" test
623+
624+ buffer [reader .end ] = '\n ' ;
625+ const line = buffer [reader .seek .. reader .end :'\n ' ];
626+ it .discard_until_newline = true ;
627+
628+ return line ;
629+ },
630+ };
631+ }
632+ };
633+
552634/// Contains the GUIDs for each available known-folder on windows
553635const WindowsFolderSpec = union (enum ) {
554636 by_guid : std.os.windows.GUID ,
@@ -891,6 +973,34 @@ test "getPath - user-dirs.dirs - duplicate config" {
891973 });
892974}
893975
976+ test "getPath - user-dirs.dirs - trailing newline" {
977+ if (builtin .os .tag == .windows ) return error .SkipZigTest ;
978+
979+ var system : TestingSystem = .{
980+ .config = .{ .xdg_on_mac = true },
981+ .env_map = &.{
982+ .{ .key = "HOME" , .value = "/home/janedoe" },
983+ .{ .key = "XDG_CONFIG_HOME" , .value = "/home/janedoe/custom" },
984+
985+ .{ .key = "XDG_VIDEOS_DIR" , .value = null },
986+ },
987+ .files = &.{.{
988+ .path = "/home/janedoe/custom/user-dirs.dirs" ,
989+ .data =
990+ \\XDG_VIDEOS_DIR="/mnt/Videos"
991+ \\
992+ ,
993+ }},
994+ };
995+ defer system .deinit ();
996+
997+ try testGetPath (.{
998+ .system = system ,
999+ .folder = .videos ,
1000+ .expected = "/mnt/Videos" ,
1001+ });
1002+ }
1003+
8941004test "getPath - user-dirs.dirs - escaped path" {
8951005 if (builtin .os .tag == .windows ) return error .SkipZigTest ;
8961006
0 commit comments