@@ -423,6 +423,163 @@ private struct URLTests {
423423 try FileManager . default. removeItem ( at: URL ( filePath: " \( tempDirectory. path) /tmp-dir " ) )
424424 }
425425
426+ #if FOUNDATION_FRAMEWORK
427+ @Test func fileSystemRepresentations( ) throws {
428+ let base = " /base/ "
429+ let pathNFC = " /caf \u{E9} "
430+ let relativeNFC = " caf \u{E9} "
431+ let pathNFD = " /cafe \u{301} "
432+ let relativeNFD = " cafe \u{301} "
433+
434+ let resolvedPathNFC = " /base/caf \u{E9} "
435+ let resolvedPathNFD = " /base/cafe \u{301} "
436+ let baseExtensionNFD = " /base.cafe \u{301} "
437+ let doubleCafeNFD = " /cafe \u{301} /cafe \u{301} "
438+
439+ // URL(filePath:) should always convert the input to decomposed (NFD) representation
440+ let baseURL = URL ( filePath: base)
441+ let urlNFC = URL ( filePath: pathNFC)
442+ let urlRelativeNFC = URL ( filePath: relativeNFC, relativeTo: baseURL)
443+ let urlNFD = URL ( filePath: pathNFD)
444+ let urlRelativeNFD = URL ( filePath: relativeNFD, relativeTo: baseURL)
445+
446+ func equalBytes( _ p1: UnsafePointer < CChar > , _ p2: UnsafePointer < CChar > ) -> Bool {
447+ return strcmp ( p1, p2) == 0
448+ }
449+
450+ // Compare bytes to ensure we have the right representation
451+ #expect( equalBytes ( urlNFC. path, pathNFD) )
452+ #expect( equalBytes ( urlNFD. path, pathNFD) )
453+ #expect( urlNFC == urlNFD)
454+
455+ #expect( equalBytes ( urlRelativeNFC. path, resolvedPathNFD) )
456+ #expect( equalBytes ( urlRelativeNFD. path, resolvedPathNFD) )
457+ #expect( urlRelativeNFC == urlRelativeNFD)
458+
459+ // withUnsafeFileSystemRepresentation should return a pointer to decomposed bytes
460+ try urlNFC. withUnsafeFileSystemRepresentation { fsRep in
461+ let fsRep = try #require( fsRep)
462+ #expect( equalBytes ( fsRep, pathNFD) )
463+ }
464+
465+ try urlNFD. withUnsafeFileSystemRepresentation { fsRep in
466+ let fsRep = try #require( fsRep)
467+ #expect( equalBytes ( fsRep, pathNFD) )
468+ }
469+
470+ try urlRelativeNFC. withUnsafeFileSystemRepresentation { fsRep in
471+ let fsRep = try #require( fsRep)
472+ #expect( equalBytes ( fsRep, resolvedPathNFD) )
473+ }
474+
475+ try urlRelativeNFD. withUnsafeFileSystemRepresentation { fsRep in
476+ let fsRep = try #require( fsRep)
477+ #expect( equalBytes ( fsRep, resolvedPathNFD) )
478+ }
479+
480+ // ...unless we specifically .init(fileURLWithFileSystemRepresentation:) with absolute NFC
481+ let urlNFCFSR = URL ( fileURLWithFileSystemRepresentation: pathNFC, isDirectory: false , relativeTo: nil )
482+ let urlNFDFSR = URL ( fileURLWithFileSystemRepresentation: pathNFD, isDirectory: false , relativeTo: nil )
483+
484+ #expect( equalBytes ( urlNFCFSR. path, pathNFC) )
485+ #expect( equalBytes ( urlNFDFSR. path, pathNFD) )
486+ #expect( urlNFCFSR != urlNFDFSR)
487+
488+ try urlNFCFSR. withUnsafeFileSystemRepresentation { fsRep in
489+ let fsRep = try #require( fsRep)
490+ #expect( equalBytes ( fsRep, pathNFC) )
491+ }
492+
493+ try urlNFDFSR. withUnsafeFileSystemRepresentation { fsRep in
494+ let fsRep = try #require( fsRep)
495+ #expect( equalBytes ( fsRep, pathNFD) )
496+ }
497+
498+ // If we .init(fileURLWithFileSystemRepresentation:) with a relative path,
499+ // we store the given representation but must convert when returning it
500+ let urlRelativeNFCFSR = URL ( fileURLWithFileSystemRepresentation: relativeNFC, isDirectory: false , relativeTo: baseURL)
501+ let urlRelativeNFDFSR = URL ( fileURLWithFileSystemRepresentation: relativeNFD, isDirectory: false , relativeTo: baseURL)
502+
503+ #expect( equalBytes ( urlRelativeNFCFSR. path, resolvedPathNFC) )
504+ #expect( equalBytes ( urlRelativeNFDFSR. path, resolvedPathNFD) )
505+ #expect( urlRelativeNFCFSR != urlRelativeNFDFSR)
506+
507+ try urlRelativeNFCFSR. withUnsafeFileSystemRepresentation { fsRep in
508+ let fsRep = try #require( fsRep)
509+ #expect( equalBytes ( fsRep, resolvedPathNFD) )
510+ }
511+
512+ try urlRelativeNFDFSR. withUnsafeFileSystemRepresentation { fsRep in
513+ let fsRep = try #require( fsRep)
514+ #expect( equalBytes ( fsRep, resolvedPathNFD) )
515+ }
516+
517+ // Appending a path component should convert to decomposed for file URLs
518+ let baseWithNFCComponent = baseURL. appending ( path: relativeNFC)
519+ #expect( equalBytes ( baseWithNFCComponent. path, resolvedPathNFD) )
520+
521+ let baseWithNFDComponent = baseURL. appending ( path: relativeNFD)
522+ #expect( equalBytes ( baseWithNFDComponent. path, resolvedPathNFD) )
523+ #expect( baseWithNFCComponent == baseWithNFDComponent)
524+
525+ let urlNFCWithNFCComponent = urlNFC. appending ( path: relativeNFC)
526+ let urlNFCWithNFDComponent = urlNFC. appending ( path: relativeNFD)
527+ let urlNFDWithNFCComponent = urlNFD. appending ( path: relativeNFC)
528+ let urlNFDWithNFDComponent = urlNFD. appending ( path: relativeNFD)
529+ #expect( equalBytes ( urlNFCWithNFCComponent. path, doubleCafeNFD) )
530+ #expect( equalBytes ( urlNFCWithNFDComponent. path, doubleCafeNFD) )
531+ #expect( equalBytes ( urlNFDWithNFCComponent. path, doubleCafeNFD) )
532+ #expect( equalBytes ( urlNFDWithNFDComponent. path, doubleCafeNFD) )
533+ #expect( urlNFCWithNFCComponent == urlNFCWithNFDComponent)
534+ #expect( urlNFCWithNFCComponent == urlNFDWithNFCComponent)
535+ #expect( urlNFCWithNFCComponent == urlNFDWithNFDComponent)
536+
537+ // Appending an extension should convert to decomposed for file URLs
538+ let baseWithNFCExtension = baseURL. appendingPathExtension ( relativeNFC)
539+ #expect( equalBytes ( baseWithNFCExtension. path, baseExtensionNFD) )
540+
541+ let baseWithNFDExtension = baseURL. appendingPathExtension ( relativeNFD)
542+ #expect( equalBytes ( baseWithNFDExtension. path, baseExtensionNFD) )
543+ #expect( baseWithNFCExtension == baseWithNFDExtension)
544+
545+ // None of these conversions apply for initializing or appending to non-file URLs
546+ let httpBase = try #require( URL ( string: " https://example.com/ " ) )
547+ let httpRelativeNFC = try #require( URL ( string: relativeNFC, relativeTo: httpBase) )
548+ let httpRelativeNFD = try #require( URL ( string: relativeNFD, relativeTo: httpBase) )
549+ let httpWithNFCComponent = httpBase. appending ( path: relativeNFC)
550+ let httpWithNFDComponent = httpBase. appending ( path: relativeNFD)
551+
552+ #expect( equalBytes ( httpRelativeNFC. path, pathNFC) )
553+ #expect( equalBytes ( httpRelativeNFD. path, pathNFD) )
554+ #expect( httpRelativeNFC != httpRelativeNFD)
555+
556+ #expect( equalBytes ( httpWithNFCComponent. path, pathNFC) )
557+ #expect( equalBytes ( httpWithNFDComponent. path, pathNFD) )
558+ #expect( httpWithNFCComponent != httpWithNFDComponent)
559+
560+ // Except when we explicitly get the file system representation
561+ try httpRelativeNFC. withUnsafeFileSystemRepresentation { fsRep in
562+ let fsRep = try #require( fsRep)
563+ #expect( equalBytes ( fsRep, pathNFD) )
564+ }
565+
566+ try httpRelativeNFD. withUnsafeFileSystemRepresentation { fsRep in
567+ let fsRep = try #require( fsRep)
568+ #expect( equalBytes ( fsRep, pathNFD) )
569+ }
570+
571+ try httpWithNFCComponent. withUnsafeFileSystemRepresentation { fsRep in
572+ let fsRep = try #require( fsRep)
573+ #expect( equalBytes ( fsRep, pathNFD) )
574+ }
575+
576+ try httpWithNFDComponent. withUnsafeFileSystemRepresentation { fsRep in
577+ let fsRep = try #require( fsRep)
578+ #expect( equalBytes ( fsRep, pathNFD) )
579+ }
580+ }
581+ #endif
582+
426583 #if os(Windows)
427584 @Test func windowsDriveLetterPath( ) throws {
428585 var url = URL ( filePath: #"C:\test\path"# , directoryHint: . notDirectory)
0 commit comments