1111 * - trimLeadingDotSlash() removes ./ prefix
1212 * - pathLikeToString() converts Buffer/URL to string
1313 * - relativeResolve() resolves relative paths
14+ * - toUnixPath() converts Windows paths to Unix-style POSIX paths for Git Bash tools
1415 * Used throughout Socket tools for cross-platform path handling.
1516 */
1617
@@ -24,6 +25,7 @@ import {
2425 pathLikeToString ,
2526 relativeResolve ,
2627 splitPath ,
28+ toUnixPath ,
2729 trimLeadingDotSlash ,
2830} from '@socketsecurity/lib/paths/normalize'
2931
@@ -358,6 +360,183 @@ describe('paths/normalize', () => {
358360 } )
359361 } )
360362
363+ describe ( 'toUnixPath' , ( ) => {
364+ const isWindows = process . platform === 'win32'
365+
366+ it ( 'should convert Windows drive letter paths with backslashes' , ( ) => {
367+ if ( isWindows ) {
368+ expect ( toUnixPath ( 'C:\\Users\\name\\file.txt' ) ) . toBe (
369+ '/c/Users/name/file.txt' ,
370+ )
371+ expect ( toUnixPath ( 'D:\\projects\\foo\\bar' ) ) . toBe ( '/d/projects/foo/bar' )
372+ }
373+ } )
374+
375+ it ( 'should convert Windows drive letter paths with forward slashes' , ( ) => {
376+ if ( isWindows ) {
377+ expect ( toUnixPath ( 'C:/Windows/System32' ) ) . toBe ( '/c/Windows/System32' )
378+ expect ( toUnixPath ( 'D:/data/logs' ) ) . toBe ( '/d/data/logs' )
379+ }
380+ } )
381+
382+ it ( 'should convert uppercase drive letters to lowercase' , ( ) => {
383+ if ( isWindows ) {
384+ expect ( toUnixPath ( 'C:\\path' ) ) . toBe ( '/c/path' )
385+ expect ( toUnixPath ( 'D:\\path' ) ) . toBe ( '/d/path' )
386+ expect ( toUnixPath ( 'Z:\\path' ) ) . toBe ( '/z/path' )
387+ }
388+ } )
389+
390+ it ( 'should handle lowercase drive letters' , ( ) => {
391+ if ( isWindows ) {
392+ expect ( toUnixPath ( 'c:\\path' ) ) . toBe ( '/c/path' )
393+ expect ( toUnixPath ( 'd:\\path' ) ) . toBe ( '/d/path' )
394+ }
395+ } )
396+
397+ it ( 'should handle mixed case drive letters' , ( ) => {
398+ if ( isWindows ) {
399+ expect ( toUnixPath ( 'c:\\Windows\\System32' ) ) . toBe ( '/c/Windows/System32' )
400+ expect ( toUnixPath ( 'D:\\Users\\John' ) ) . toBe ( '/d/Users/John' )
401+ }
402+ } )
403+
404+ it ( 'should handle UNC paths' , ( ) => {
405+ if ( isWindows ) {
406+ expect ( toUnixPath ( '\\\\server\\share\\file' ) ) . toBe (
407+ '//server/share/file' ,
408+ )
409+ expect ( toUnixPath ( '\\\\server\\share\\path\\to\\file' ) ) . toBe (
410+ '//server/share/path/to/file' ,
411+ )
412+ }
413+ } )
414+
415+ it ( 'should handle Unix absolute paths on Unix' , ( ) => {
416+ if ( ! isWindows ) {
417+ expect ( toUnixPath ( '/home/user/file' ) ) . toBe ( '/home/user/file' )
418+ expect ( toUnixPath ( '/usr/local/bin' ) ) . toBe ( '/usr/local/bin' )
419+ expect ( toUnixPath ( '/var/log/app.log' ) ) . toBe ( '/var/log/app.log' )
420+ }
421+ } )
422+
423+ it ( 'should normalize paths on Unix (collapse .., remove ./, etc)' , ( ) => {
424+ if ( ! isWindows ) {
425+ // Verify that normalization still happens on Unix
426+ expect ( toUnixPath ( '/usr/local/../bin' ) ) . toBe ( '/usr/bin' )
427+ expect ( toUnixPath ( '/usr//local///bin' ) ) . toBe ( '/usr/local/bin' )
428+ expect ( toUnixPath ( './src/index.ts' ) ) . toBe ( 'src/index.ts' )
429+ expect ( toUnixPath ( '/usr/./local/bin' ) ) . toBe ( '/usr/local/bin' )
430+ }
431+ } )
432+
433+ it ( 'should handle relative paths' , ( ) => {
434+ // Relative paths get normalized but don't get drive letter conversion
435+ const result1 = toUnixPath ( './src/index.ts' )
436+ const result2 = toUnixPath ( '../lib/utils' )
437+ expect ( result1 ) . toContain ( 'src' )
438+ expect ( result2 ) . toContain ( 'lib' )
439+ // On Unix, should be unchanged. On Windows, backslashes become forward slashes
440+ expect ( result1 . includes ( '\\\\' ) ) . toBe ( false )
441+ expect ( result2 . includes ( '\\\\' ) ) . toBe ( false )
442+ } )
443+
444+ it ( 'should handle Buffer input' , ( ) => {
445+ if ( isWindows ) {
446+ const buffer = Buffer . from ( 'C:\\Users\\name' )
447+ expect ( toUnixPath ( buffer ) ) . toBe ( '/c/Users/name' )
448+ } else {
449+ const buffer = Buffer . from ( '/usr/local' )
450+ expect ( toUnixPath ( buffer ) ) . toBe ( '/usr/local' )
451+ }
452+ } )
453+
454+ it ( 'should handle URL input' , ( ) => {
455+ if ( isWindows ) {
456+ const url = new URL ( 'file:///C:/Windows/System32' )
457+ const result = toUnixPath ( url )
458+ expect ( result ) . toContain ( '/c/' )
459+ expect ( result ) . toContain ( 'Windows' )
460+ } else {
461+ const url = new URL ( 'file:///usr/local' )
462+ const result = toUnixPath ( url )
463+ expect ( result ) . toContain ( '/usr/local' )
464+ }
465+ } )
466+
467+ it ( 'should handle empty string' , ( ) => {
468+ // Empty string normalizes to '.' on all platforms (consistent with Node.js path.normalize)
469+ expect ( toUnixPath ( '' ) ) . toBe ( '.' )
470+ } )
471+
472+ it ( 'should handle root paths' , ( ) => {
473+ if ( ! isWindows ) {
474+ expect ( toUnixPath ( '/' ) ) . toBe ( '/' )
475+ }
476+ } )
477+
478+ it ( 'should handle paths with spaces' , ( ) => {
479+ if ( isWindows ) {
480+ expect ( toUnixPath ( 'C:\\Program Files\\App' ) ) . toBe (
481+ '/c/Program Files/App' ,
482+ )
483+ expect ( toUnixPath ( 'D:\\My Documents\\file.txt' ) ) . toBe (
484+ '/d/My Documents/file.txt' ,
485+ )
486+ }
487+ } )
488+
489+ it ( 'should handle paths with special characters' , ( ) => {
490+ if ( isWindows ) {
491+ expect ( toUnixPath ( 'C:\\Users\\name\\file (1).txt' ) ) . toBe (
492+ '/c/Users/name/file (1).txt' ,
493+ )
494+ expect ( toUnixPath ( 'D:\\projects\\@scope\\package' ) ) . toBe (
495+ '/d/projects/@scope/package' ,
496+ )
497+ }
498+ } )
499+
500+ it ( 'should handle mixed separators in path' , ( ) => {
501+ if ( isWindows ) {
502+ expect ( toUnixPath ( 'C:\\Users/name\\file.txt' ) ) . toBe (
503+ '/c/Users/name/file.txt' ,
504+ )
505+ }
506+ } )
507+
508+ it ( 'should handle all drive letters A-Z' , ( ) => {
509+ if ( isWindows ) {
510+ expect ( toUnixPath ( 'A:\\path' ) ) . toBe ( '/a/path' )
511+ expect ( toUnixPath ( 'E:\\path' ) ) . toBe ( '/e/path' )
512+ expect ( toUnixPath ( 'Z:\\path' ) ) . toBe ( '/z/path' )
513+ }
514+ } )
515+
516+ it ( 'should preserve path after drive letter conversion' , ( ) => {
517+ if ( isWindows ) {
518+ expect ( toUnixPath ( 'C:\\a\\b\\c\\d\\e\\f' ) ) . toBe ( '/c/a/b/c/d/e/f' )
519+ expect ( toUnixPath ( 'D:\\projects\\socket-btm\\build\\dev' ) ) . toBe (
520+ '/d/projects/socket-btm/build/dev' ,
521+ )
522+ }
523+ } )
524+
525+ it ( 'should handle Git Bash tar paths correctly' , ( ) => {
526+ // This is the primary use case: Git for Windows tar.EXE needs POSIX paths
527+ if ( isWindows ) {
528+ // Example from Windows CI: D:\a\socket-btm\build\dev
529+ expect ( toUnixPath ( 'D:\\a\\socket-btm\\build\\dev' ) ) . toBe (
530+ '/d/a/socket-btm/build/dev' ,
531+ )
532+ // tar expects /d/path not D:\path
533+ const result = toUnixPath ( 'C:\\Windows\\Temp\\archive.tar.gz' )
534+ expect ( result . startsWith ( '/c/' ) ) . toBe ( true )
535+ expect ( result . includes ( '\\' ) ) . toBe ( false )
536+ }
537+ } )
538+ } )
539+
361540 describe ( 'Edge cases' , ( ) => {
362541 it ( 'should handle very long paths' , ( ) => {
363542 const longPath = `/usr/${ 'a/' . repeat ( 100 ) } file.txt`
0 commit comments