diff --git a/Package.swift b/Package.swift index 7f774e65..9b089b89 100644 --- a/Package.swift +++ b/Package.swift @@ -85,6 +85,15 @@ let swiftSettings = swiftSettingsAvailability + swiftSettingsCI + [ .define("SYSTEM_PACKAGE"), .define("ENABLE_MOCKING", .when(configuration: .debug)), .enableExperimentalFeature("Lifetimes"), + // FilePath port: select the SE-0529 sources' package-mode branch (provides + // _internalInvariant etc.) and define the SwiftStdlib availability macro their + // @available annotations reference. Mirrors ../SE-0529-FilePath/Package.swift; + // that package's strictMemorySafety/-Werror are enforcement, not source seam, + // so are intentionally not brought over. + .define("FILEPATH_PACKAGE"), + .enableExperimentalFeature( + "AvailabilityMacro=SwiftStdlib 9999:macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + .unsafeFlags(["-Xfrontend", "-disable-availability-checking"]), ] let cSettings: [CSetting] = [ diff --git a/Sources/System/FilePath/FilePath+SystemPackage.swift b/Sources/System/FilePath/FilePath+SystemPackage.swift new file mode 100644 index 00000000..e69de29b diff --git a/Sources/System/FilePath/FilePath.swift b/Sources/System/FilePath/FilePath.swift deleted file mode 100644 index b27d053a..00000000 --- a/Sources/System/FilePath/FilePath.swift +++ /dev/null @@ -1,90 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 Apple Inc. and the Swift System project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See https://swift.org/LICENSE.txt for license information -*/ - -/// Represents a location in the file system. -/// -/// This structure recognizes directory separators (e.g. `/`), roots, and -/// requires that the content terminates in a NUL (`0x0`). Beyond that, it -/// does not give any meaning to the bytes that it contains. The file system -/// defines how the content is interpreted; for example, by its choice of string -/// encoding. -/// -/// On construction, `FilePath` will normalize separators by removing -/// redundant intermediary separators and stripping any trailing separators. -/// On Windows, `FilePath` will also normalize forward slashes `/` into -/// backslashes `\`, as preferred by the platform. -/// -/// The code below creates a file path from a string literal, -/// and then uses it to open and append to a log file: -/// -/// let message: String = "This is a log message." -/// let path: FilePath = "/tmp/log" -/// let fd = try FileDescriptor.open(path, .writeOnly, options: .append) -/// try fd.closeAfter { try fd.writeAll(message.utf8) } -/// -/// File paths conform to the -/// -/// and protocols -/// by performing the protocols' operations on their raw byte contents. -/// This conformance allows file paths to be used, -/// for example, as keys in a dictionary. -/// However, the rules for path equivalence -/// are file-system–specific and have additional considerations -/// like case insensitivity, Unicode normalization, and symbolic links. -@available(System 0.0.1, *) -public struct FilePath: Sendable { - // TODO(docs): Section on all the new syntactic operations, lexical normalization, decomposition, - // components, etc. - internal var _storage: SystemString - - /// Creates an empty, null-terminated path. - public init() { - self._storage = SystemString() - _invariantCheck() - } - - // In addition to the empty init, this init will properly normalize - // separators. All other initializers should be implemented by - // ultimately deferring to a normalizing init. - internal init(_ str: SystemString) { - self._storage = str - self._normalizeSeparators() - _invariantCheck() - } -} - -@available(System 0.0.1, *) -extension FilePath { - /// The length of the file path, excluding the null terminator. - public var length: Int { _storage.length } -} - -@available(System 0.0.1, *) -extension FilePath: Hashable {} - -@available(System 0.0.1, *) -extension FilePath: Codable { - // Encoder is synthesized; it probably should have been explicit and used - // a single-value container, but making that change now is somewhat risky. - - // Decoder is written explicitly to ensure that we validate invariants on - // untrusted input. - public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self._storage = try container.decode(SystemString.self, forKey: ._storage) - guard _invariantsSatisfied() else { - throw DecodingError.dataCorruptedError( - forKey: ._storage, - in: container, - debugDescription: - "Encoding does not satisfy the invariants of FilePath" - ) - } - } -} diff --git a/Sources/System/FilePath/FilePathCompatInternals.swift b/Sources/System/FilePath/FilePathCompatInternals.swift new file mode 100644 index 00000000..ec74e78d --- /dev/null +++ b/Sources/System/FilePath/FilePathCompatInternals.swift @@ -0,0 +1,89 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2026 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +// PORT SHIM: re-plumbs the internal `FilePath` helpers that the old package +// API (FilePathSyntax.swift, FilePathString.swift, …) still calls onto the +// SE-0529 stdlib copy's internals. These names used to live in the old +// FilePath implementation files that were emptied during the port; the +// bodies below are reimplemented on the copy's `_storage: _SystemString`, +// its `_parseRoot()` boundaries, and the `_normalizing` construction funnel. +// +// Temporary by design: once the call sites move to the copy's own API +// (`anchor`, `hasTrailingSeparator`, `components`, …) these compat members +// and their call sites are the mechanical removal list. Do not grow API here. + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// Old debug invariant hook. The stdlib copy has no whole-path recheck + /// method (it asserts inline via `_internalInvariant` and establishes + /// invariants at construction), so this checks the invariant directly: + /// storage must be a fixed point of the `_normalizing` funnel. + internal func _invariantCheck() { + #if DEBUG + // The copy's storage invariant, stated as a property: normal form is a + // fixed point of the normalizing funnel. Catches any old-API code path + // that mutates _storage directly and leaves it non-normal. + let renormalized = FilePath(_normalizing: _storage) + precondition( + renormalized._storage == self._storage, + "FilePath storage not in stdlib normal form") + #endif + } + + /// Whether the path begins with an anchor/root. + /// + /// Mirrors the copy's `anchor != nil`: the root is non-empty iff parsing + /// finds an anchor that ends past the start of storage. + internal var _hasRoot: Bool { + _storage._parseRoot().rootEnd != _storage.startIndex + } + + /// The index in `_storage` where the relative portion begins, i.e. just + /// past the anchor and any gap separator. Equivalent to the copy's + /// `_parseRoot().relativeBegin`. + internal var _relativeStart: _SystemString.Index { + _storage._parseRoot().relativeBegin + } + + /// Append raw path bytes, inserting a platform separator between existing + /// content and the new bytes when the current storage does not already end + /// in one. Routes the result through the copy's `_normalizing` funnel so + /// storage keeps the copy's invariants (coalesced separators, dot rules). + internal mutating func _append( + unchecked newElements: some Collection + ) { + guard !newElements.isEmpty else { return } + var storage = _storage + if !storage.isEmpty, !_isSeparator(storage.last!) { + storage.append(_platformSeparator) + } + storage.append(contentsOf: newElements) + self = FilePath(_normalizing: storage) + } + + /// Drop a trailing directory separator, if the path has a non-structural + /// one. Delegates to the copy's `hasTrailingSeparator` setter, which + /// preserves separators that belong to the anchor (e.g. `\\server\share\`). + internal mutating func _removeTrailingSeparator() { + hasTrailingSeparator = false + } + + /// Lexically collapse `.` and `..` components in place. + internal mutating func _normalizeSpecialDirectories() { + // PORT-TODO: no stdlib-copy equivalent. The copy normalizes at + // construction (`_SystemString._normalizeDots`) but deliberately + // PRESERVES `..`, whereas the old `_normalizeSpecialDirectories` + // lexically resolved `..` against preceding components (this is what + // `lexicallyNormalize()` relied on). The copy exposes no lexical-collapse + // primitive, so this needs a real port; left unimplemented rather than + // inventing semantics. + fatalError( + "PORT-TODO: _normalizeSpecialDirectories has no stdlib-copy equivalent") + } +} diff --git a/Sources/System/FilePath/FilePathComponentCompat.swift b/Sources/System/FilePath/FilePathComponentCompat.swift new file mode 100644 index 00000000..a5be969c --- /dev/null +++ b/Sources/System/FilePath/FilePathComponentCompat.swift @@ -0,0 +1,51 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2026 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +// PORT SHIM: internal helpers for FilePath.Component that the old package +// API (Component.extension / Component.stem in FilePathSyntax.swift) relies +// on. The SE-0529 copy has no stem/extension notion, so these are the old +// swift-system algorithms retyped from the old substrate (SystemChar, +// Slice) onto the copy's (FilePath.CodeUnit, +// Slice<_SystemString>). Algorithm bodies are unchanged from the originals +// in FilePathComponents.swift (now PORT-CLOBBERED there). + +@available(SwiftStdlib 9999, *) +extension Slice where Base == _SystemString { + /// Decode this slice's code units as a String. Mirrors + /// `_SystemString.string` in the stdlib copy. + internal var string: String { + withCodeUnits { codeUnits in + codeUnits.withMemoryRebound(to: FilePath._Encoding.CodeUnit.self) { + String(decoding: $0, as: FilePath._Encoding.self) + } + } + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Component { + // The index of the `.` denoting an extension + internal func _extensionIndex() -> _SystemString.Index? { + guard kind == .regular, + let idx = _slice.lastIndex(of: ._dot), + idx != _slice.startIndex + else { return nil } + + return idx + } + + internal func _extensionRange() -> Range<_SystemString.Index>? { + guard let idx = _extensionIndex() else { return nil } + return _slice.index(after: idx) ..< _slice.endIndex + } + + internal func _stemRange() -> Range<_SystemString.Index> { + _slice.startIndex ..< (_extensionIndex() ?? _slice.endIndex) + } +} diff --git a/Sources/System/FilePath/FilePathComponentView+SystemPackage.swift b/Sources/System/FilePath/FilePathComponentView+SystemPackage.swift new file mode 100644 index 00000000..e69de29b diff --git a/Sources/System/FilePath/FilePathComponentView.swift b/Sources/System/FilePath/FilePathComponentView.swift deleted file mode 100644 index be176305..00000000 --- a/Sources/System/FilePath/FilePathComponentView.swift +++ /dev/null @@ -1,219 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2020 Apple Inc. and the Swift System project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See https://swift.org/LICENSE.txt for license information -*/ - -// MARK: - API - -@available(System 0.0.2, *) -extension FilePath { - /// A bidirectional, range replaceable collection of the non-root components - /// that make up a file path. - /// - /// ComponentView provides access to standard `BidirectionalCollection` - /// algorithms for accessing components from the front or back, as well as - /// standard `RangeReplaceableCollection` algorithms for modifying the - /// file path using component or range of components granularity. - /// - /// Example: - /// - /// var path: FilePath = "/./home/./username/scripts/./tree" - /// let scriptIdx = path.components.lastIndex(of: "scripts")! - /// path.components.insert("bin", at: scriptIdx) - /// // path is "/./home/./username/bin/scripts/./tree" - /// - /// path.components.removeAll { $0.kind == .currentDirectory } - /// // path is "/home/username/bin/scripts/tree" - @available(System 0.0.2, *) - public struct ComponentView: Sendable { - internal var _path: FilePath - internal var _start: SystemString.Index - - internal init(_ path: FilePath) { - self._path = path - self._start = path._relativeStart - _invariantCheck() - } - } - - /// View the non-root components that make up this path. - public var components: ComponentView { - __consuming get { ComponentView(self) } - _modify { - // RRC's empty init means that we can't guarantee that the yielded - // view will restore our root. So copy it out first. - // - // TODO(perf): Small-form root (especially on Unix). Have Root - // always copy out (not worth ref counting). Make sure that we're - // not needlessly sliding values around or triggering a COW - let rootStr = self.root?._systemString ?? SystemString() - var comp = ComponentView(self) - self = FilePath() - defer { - self = comp._path - if root?._slice.elementsEqual(rootStr) != true { - self.root = Root(rootStr) - } - } - yield &comp - } - } -} - -@available(System 0.0.2, *) -extension FilePath.ComponentView: BidirectionalCollection { - public typealias Element = FilePath.Component - - @available(System 0.0.2, *) - public struct Index: Sendable, Comparable, Hashable { - internal typealias Storage = SystemString.Index - - internal var _storage: Storage - - public static func < (lhs: Self, rhs: Self) -> Bool { - lhs._storage < rhs._storage - } - - fileprivate init(_ idx: Storage) { - self._storage = idx - } - } - - public var startIndex: Index { Index(_start) } - public var endIndex: Index { Index(_path._storage.endIndex) } - - public func index(after i: Index) -> Index { - return Index(_path._parseComponent(startingAt: i._storage).nextStart) - } - - public func index(before i: Index) -> Index { - Index(_path._parseComponent(priorTo: i._storage).lowerBound) - } - - public subscript(position: Index) -> FilePath.Component { - let end = _path._parseComponent(startingAt: position._storage).componentEnd - return FilePath.Component(_path, position._storage ..< end) - } -} - -@available(System 0.0.2, *) -extension FilePath.ComponentView: RangeReplaceableCollection { - public init() { - self.init(FilePath()) - } - - // TODO(perf): We probably want to have concrete overrides or generic - // specializations taking FP.ComponentView and - // FP.ComponentView.SubSequence because we - // can just memcpy in those cases. We - // probably want to do that for all RRC operations. - - public mutating func replaceSubrange( - _ subrange: Range, with newElements: C - ) where C : Collection, Self.Element == C.Element { - defer { - _path._invariantCheck() - _invariantCheck() - } - if isEmpty { - _path = FilePath(root: _path.root, newElements) - return - } - let range = subrange.lowerBound._storage ..< subrange.upperBound._storage - if newElements.isEmpty { - let fromEnd = subrange.upperBound == endIndex - _path._storage.removeSubrange(range) - if fromEnd { - _path._removeTrailingSeparator() - } - return - } - - // TODO(perf): Avoid extra allocation by sliding elements down and - // filling in the bytes ourselves. - - // If we're inserting at the end, we need a leading separator. - var str = SystemString() - let atEnd = subrange.lowerBound == endIndex - if atEnd { - str.append(platformSeparator) - } - str.appendComponents(components: newElements) - if !atEnd { - str.append(platformSeparator) - } - _path._storage.replaceSubrange(range, with: str) - } -} - -@available(System 0.0.2, *) -extension FilePath { - /// Create a file path from a root and a collection of components. - public init( - root: Root?, _ components: C - ) where C.Element == Component { - var str = root?._systemString ?? SystemString() - str.appendComponents(components: components) - self.init(str) - } - - /// Create a file path from a root and any number of components. - public init(root: Root?, components: Component...) { - self.init(root: root, components) - } - - /// Create a file path from an optional root and a slice of another path's - /// components. - public init(root: Root?, _ components: ComponentView.SubSequence) { - var str = root?._systemString ?? SystemString() - let (start, end) = - (components.startIndex._storage, components.endIndex._storage) - str.append(contentsOf: components.base._slice[start.. { - _start ..< _path._storage.endIndex - } - - internal init(_ str: SystemString) { - fatalError("TODO: consider dropping proto req") - } -} - -// MARK: - Invariants - -@available(System 0.0.2, *) -extension FilePath.ComponentView { - internal func _invariantCheck() { - #if DEBUG - if isEmpty { - precondition(_path.isEmpty == (_path.root == nil)) - return - } - - // If path has a root, - if _path.root != nil { - precondition(first!._slice.startIndex > _path._storage.startIndex) - precondition(first!._slice.startIndex == _path._relativeStart) - } - - self.forEach { $0._invariantCheck() } - - if let base = last { - precondition(base._slice.endIndex == _path._storage.endIndex) - } - - precondition(FilePath(root: _path.root, self) == _path) - #endif // DEBUG - } -} diff --git a/Sources/System/FilePath/FilePathComponents.swift b/Sources/System/FilePath/FilePathComponents.swift index f2352617..789cb318 100644 --- a/Sources/System/FilePath/FilePathComponents.swift +++ b/Sources/System/FilePath/FilePathComponents.swift @@ -55,6 +55,7 @@ extension FilePath { /// file.kind == .regular // true /// file.extension // "txt" /// path.append(file) // path is "/tmp/foo.txt" +#if false // PORT-CLOBBERED: superseded by stdlib copy @available(System 0.0.2, *) public struct Component: Sendable { internal var _path: FilePath @@ -72,8 +73,10 @@ extension FilePath { self._invariantCheck() } } +#endif } +#if false // PORT-CLOBBERED: superseded by stdlib copy @available(System 0.0.2, *) extension FilePath.Component { @@ -99,6 +102,7 @@ extension FilePath.Component { return .regular } } +#endif @available(System 0.0.2, *) extension FilePath.Root { @@ -107,6 +111,8 @@ extension FilePath.Root { // MARK: - Internals +#if false // PORT-CLOBBERED: dead code; its callers died with the old +// ComponentView, and the stdlib copy's ComponentView machinery supersedes it. extension SystemString { // TODO: take insertLeadingSlash: Bool // TODO: turn into an insert operation with slide @@ -127,46 +133,57 @@ extension SystemString { } } } +#endif // Unifying protocol for common functionality between roots, components, // and views onto SystemString and FilePath. internal protocol _StrSlice: _PlatformStringable, Hashable, Codable { - var _storage: SystemString { get } - var _range: Range { get } + var _storage: _SystemString { get } + var _range: Range<_SystemString.Index> { get } - init?(_ str: SystemString) + init?(_ str: _SystemString) func _invariantCheck() } extension _StrSlice { - internal var _slice: Slice { + internal var _slice: Slice<_SystemString> { Slice(base: _storage, bounds: _range) } internal func _withSystemChars( - _ f: (UnsafeBufferPointer) throws -> T + _ f: (UnsafeBufferPointer) throws -> T ) rethrows -> T { - try _storage.withNullTerminatedSystemChars { + try _storage.withNullTerminatedCodeUnits { try f(UnsafeBufferPointer(rebasing: $0[_range])) } } internal func _withCodeUnits( _ f: (UnsafeBufferPointer) throws -> T ) rethrows -> T { - try _slice.withCodeUnits(f) + try _slice.withCodeUnits { + try $0.withMemoryRebound( + to: CInterop.PlatformUnicodeEncoding.CodeUnit.self + ) { try f($0) } + } } internal init?(_platformString s: UnsafePointer) { - self.init(SystemString(platformString: s)) + self.init(_SystemString(SystemString(platformString: s))) } internal func _withPlatformString( _ body: (UnsafePointer) throws -> Result ) rethrows -> Result { - try _slice.withPlatformString(body) + var copy = _SystemString() + copy.append(contentsOf: _slice) + return try copy.withPlatformString(body) } - internal var _systemString: SystemString { SystemString(_slice) } + internal var _systemString: SystemString { + var copy = _SystemString() + copy.append(contentsOf: _slice) + return SystemString(copy) + } } extension _StrSlice { public static func == (lhs: Self, rhs: Self) -> Bool { @@ -183,7 +200,7 @@ internal protocol _PathSlice: _StrSlice { var _path: FilePath { get } } extension _PathSlice { - internal var _storage: SystemString { _path._storage } + internal var _storage: _SystemString { _path._storage } } @available(System 0.0.2, *) @@ -191,7 +208,7 @@ extension FilePath.Component: _PathSlice { } @available(System 0.0.2, *) extension FilePath.Root: _PathSlice { - internal var _range: Range { + internal var _range: Range<_SystemString.Index> { (..<_rootEnd).relative(to: _path._storage) } } @@ -208,6 +225,7 @@ extension FilePath: _PlatformStringable { } +#if false // PORT-CLOBBERED: superseded by stdlib copy @available(System 0.0.2, *) extension FilePath.Component { // The index of the `.` denoting an extension @@ -229,6 +247,7 @@ extension FilePath.Component { _slice.startIndex ..< (_extensionIndex() ?? _slice.endIndex) } } +#endif internal func _makeExtension(_ ext: String) -> SystemString { var result = SystemString() @@ -239,9 +258,9 @@ internal func _makeExtension(_ ext: String) -> SystemString { @available(System 0.0.2, *) extension FilePath.Component { - internal init?(_ str: SystemString) { + internal init?(_ str: _SystemString) { // FIXME: explicit null root? Or something else? - let path = FilePath(str) + let path = FilePath(_normalizing: str) guard path.root == nil, path.components.count == 1 else { return nil } @@ -252,9 +271,9 @@ extension FilePath.Component { @available(System 0.0.2, *) extension FilePath.Root { - internal init?(_ str: SystemString) { + internal init?(_ str: _SystemString) { // FIXME: explicit null root? Or something else? - let path = FilePath(str) + let path = FilePath(_normalizing: str) guard path.root != nil, path.components.isEmpty else { return nil } @@ -271,8 +290,8 @@ extension FilePath.Component { internal func _invariantCheck() { #if DEBUG precondition(!_slice.isEmpty) - precondition(_slice.last != .null) - precondition(_slice.allSatisfy { !isSeparator($0) } ) + precondition(_slice.last != ._null) + precondition(_slice.allSatisfy { !_isSeparator($0) } ) precondition(_path._relativeStart <= _slice.startIndex) #endif // DEBUG } diff --git a/Sources/System/FilePath/FilePathConformances.swift b/Sources/System/FilePath/FilePathConformances.swift new file mode 100644 index 00000000..2e58b5bf --- /dev/null +++ b/Sources/System/FilePath/FilePathConformances.swift @@ -0,0 +1,131 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2026 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +// Conformances swift-system's FilePath has always shipped that the stdlib +// implementation deliberately does not. SE-0529 excludes Codable from the +// stdlib FilePath by design (serialization is left to application-level +// code), so this file is permanent package-side surface, not a port shim. +// +// Wire-format compatibility contract: the historical encoding was the +// synthesized form over `_storage: SystemString`, i.e. +// { "_storage": { "nullTerminatedStorage": [code units...] } } +// SystemString still carries its original Codable (including the +// invariant-validating decoder), so encoding through it reproduces the old +// bytes exactly. + +@available(System 0.0.1, *) +extension FilePath: Codable { + private enum CodingKeys: String, CodingKey { + case _storage + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(SystemString(_storage), forKey: ._storage) + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let storage = try container.decode(SystemString.self, forKey: ._storage) + // SystemString's decoder has already validated storage invariants on + // untrusted input, matching the old explicit FilePath decoder. + // + // Construction goes through the stdlib copy's normalizing funnel: every + // payload the old decoder accepted still decodes, but the stored byte + // spelling is the copy's normal form, which can differ from the encoded + // spelling. If byte-faithful round-trips are required instead, switch to + // init(_storage:) plus a strict is-normal check (rejects some old + // payloads). + self.init(storage) + } +} + +// Historical wire format for Component and Root was synthesized over their +// stored properties: {_path, _range} and {_path, _rootEnd}, with integer +// indices into the encoded path's bytes. Decoding must slice those raw bytes +// BEFORE any normalization (the modern FilePath decode normalizes), so both +// decoders below read the path's storage through a raw mirror, slice, and +// reconstruct. Their Codable obligation comes via _StrSlice, so only the +// members are provided here, not the conformance. + +// Raw mirror of FilePath's encoded form; decodes storage without normalizing. +private struct _EncodedFilePath: Codable { + var _storage: SystemString +} + +@available(System 0.0.2, *) +extension FilePath.Component { + private enum CodingKeys: String, CodingKey { + case _path, _range, _verbatimContext + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(_path, forKey: ._path) + try container.encode(_range, forKey: ._range) + // Additive vs the historical two-key format; old decoders ignore it. + try container.encode(_verbatimContext, forKey: ._verbatimContext) + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let raw = _SystemString( + try container.decode(_EncodedFilePath.self, forKey: ._path)._storage) + let range = try container.decode(Range.self, forKey: ._range) + guard range.lowerBound >= raw.startIndex, + range.upperBound <= raw.endIndex else { + throw DecodingError.dataCorruptedError( + forKey: ._range, in: container, + debugDescription: "Component range outside encoded path storage") + } + var bytes = _SystemString() + bytes.append(contentsOf: raw[range]) + // Reconstruction re-derives _verbatimContext from the bytes; the encoded + // flag (absent in historical payloads) is not needed. + guard let component = FilePath.Component(bytes) else { + throw DecodingError.dataCorruptedError( + forKey: ._range, in: container, + debugDescription: "Encoded bytes do not form a single path component") + } + self = component + } +} + +@available(System 0.0.2, *) +extension FilePath.Root { + private enum CodingKeys: String, CodingKey { + case _path, _rootEnd + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(_path, forKey: ._path) + try container.encode(_rootEnd, forKey: ._rootEnd) + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let raw = _SystemString( + try container.decode(_EncodedFilePath.self, forKey: ._path)._storage) + let rootEnd = try container.decode(Int.self, forKey: ._rootEnd) + guard rootEnd > raw.startIndex, rootEnd <= raw.endIndex else { + throw DecodingError.dataCorruptedError( + forKey: ._rootEnd, in: container, + debugDescription: "Root end outside encoded path storage") + } + var bytes = _SystemString() + bytes.append(contentsOf: raw[raw.startIndex.. Bool { - c == platformSeparator -} - -// Whether the character is a pre-normalized separator -internal func isPrenormalSeparator(_ c: SystemChar) -> Bool { - c == genericSeparator || c == platformSeparator -} - -// Separator normalization, checking, and root parsing is internally hosted -// on SystemString for ease of unit testing. - -extension SystemString { - // For invariant enforcing/checking. Should always return false on - // a fully-formed path - fileprivate func _hasTrailingSeparator() -> Bool { - // Just a root: do nothing - guard _relativePathStart != endIndex else { return false } - assert(!isEmpty) - - return isSeparator(self.last!) - } - - // Enforce invariants by removing a trailing separator. - // - // Precondition: There is exactly zero or one trailing slashes - // - // Postcondition: Path is root, or has no trailing separator - internal mutating func _removeTrailingSeparator() { - if _hasTrailingSeparator() { - self.removeLast() - assert(!_hasTrailingSeparator()) - } - } - - // Enforce invariants by normalizing the internal separator representation. - // - // 1) Normalize all separators to platform-preferred separator - // 2) Drop redundant separators - // 3) Drop trailing separators - // - // On Windows, UNC and device paths are allowed to begin with two separators, - // and partial or mal-formed roots are completed. - // - // The POSIX standard does allow two leading separators to - // denote implementation-specific handling, but Darwin and Linux - // do not treat these differently. - // - internal mutating func _normalizeSeparators() { - guard !isEmpty else { return } - var (writeIdx, readIdx) = (startIndex, startIndex) - - if _windowsPaths { - // Normalize forwards slashes to backslashes. - // - // NOTE: Ideally this would be done as part of separator coalescing - // below. However, prenormalizing roots such as UNC paths requires - // parsing and (potentially) fixing up semi-formed roots. This - // normalization reduces the complexity of the task by allowing us to - // use a read-only lexer. - self._replaceAll(genericSeparator, with: platformSeparator) - - // Windows roots can have meaningful repeated backslashes or may - // need backslashes inserted for partially-formed roots. Delegate that to - // `_prenormalizeWindowsRoots` and resume. - readIdx = _prenormalizeWindowsRoots() - writeIdx = readIdx - - // Skip redundant separators - while readIdx < endIndex && isSeparator(self[readIdx]) { - self.formIndex(after: &readIdx) - } - } else { - assert(genericSeparator == platformSeparator) - } - - while readIdx < endIndex { - assert(writeIdx <= readIdx) - - // Swap and advance our indices. - let wasSeparator = isSeparator(self[readIdx]) - self.swapAt(writeIdx, readIdx) - self.formIndex(after: &writeIdx) - self.formIndex(after: &readIdx) - - while wasSeparator, readIdx < endIndex, isSeparator(self[readIdx]) { - self.formIndex(after: &readIdx) - } - } - self.removeLast(self.distance(from: writeIdx, to: readIdx)) - self._removeTrailingSeparator() - } -} - -@available(System 0.0.1, *) -extension FilePath { - internal mutating func _removeTrailingSeparator() { - _storage._removeTrailingSeparator() - } - - internal mutating func _normalizeSeparators() { - _storage._normalizeSeparators() - } - - // Remove any `.` and `..` components - internal mutating func _normalizeSpecialDirectories() { - guard !isLexicallyNormal else { return } - defer { assert(isLexicallyNormal) } - - let relStart = _relativeStart - let hasRoot = relStart != _storage.startIndex - - // TODO: all this logic might be nicer if _parseComponent considered - // the null character index to be the next start... - - var (writeIdx, readIdx) = (relStart, relStart) - while readIdx < _storage.endIndex { - let (compEnd, nextStart) = _parseComponent(startingAt: readIdx) - assert(readIdx < nextStart && compEnd <= nextStart) - let component = readIdx..= writeIdx) - if readIdx != writeIdx { - _storage.removeSubrange(writeIdx...) - _removeTrailingSeparator() - } - } -} - -extension SystemString { - internal var _relativePathStart: Index { - _parseRoot().relativeBegin - } -} - -@available(System 0.0.1, *) -extension FilePath { - internal var _relativeStart: SystemString.Index { - _storage._relativePathStart - } - internal var _hasRoot: Bool { - _relativeStart != _storage.startIndex - } -} - -// Parse separators - -@available(System 0.0.1, *) -extension FilePath { - internal typealias _Index = SystemString.Index - - // Parse a component that starts at `i`. Returns the end - // of the component and the start of the next. Parsing terminates - // at the index of the null byte. - internal func _parseComponent( - startingAt i: _Index - ) -> (componentEnd: _Index, nextStart: _Index) { - assert(i < _storage.endIndex) - // Parse the root - if i == _storage.startIndex { - let relativeStart = _relativeStart - if i != relativeStart { - return (relativeStart, relativeStart) - } - } - - assert(!isSeparator(_storage[i])) - guard let nextSep = _storage[i...].firstIndex(where: isSeparator) else { - return (_storage.endIndex, _storage.endIndex) - } - return (nextSep, _storage.index(after: nextSep)) - } - - // Parse a component prior to the one that starts at `i`. Returns - // the start of the prior component. If `i` is the index of null, - // returns the last component. - internal func _parseComponent( - priorTo i: _Index - ) -> Range<_Index> { - precondition(i > _storage.startIndex) - let relStart = _relativeStart - - if i == relStart { return _storage.startIndex.. relStart) - - var slice = _storage[..) -> Bool { - _storage[component].elementsEqual([.dot]) - } - - internal func _isParentDirectory(_ component: Range<_Index>) -> Bool { - _storage[component].elementsEqual([.dot, .dot]) - } - - internal func _isSpecialDirectory(_ component: Range<_Index>) -> Bool { - _isCurrentDirectory(component) || _isParentDirectory(component) - } -} - -@available(System 0.0.2, *) -extension FilePath.ComponentView { - // TODO: Store this... - internal var _relativeStart: SystemString.Index { - _path._relativeStart - } -} - -extension SystemString { - internal func _parseRoot() -> ( - rootEnd: Index, relativeBegin: Index - ) { - guard !isEmpty else { return (startIndex, startIndex) } - - // Windows roots are more complex - if _windowsPaths { return _parseWindowsRoot() } - - // A leading `/` is a root - guard isSeparator(self.first!) else { return (startIndex, startIndex) } - - let next = self.index(after: startIndex) - return (next, next) - } -} - -@available(System 0.0.2, *) -extension FilePath.Root { - // Asserting self is a root, returns whether this is an - // absolute root. - // - // On Unix, all roots are absolute. On Windows, `\` and `X:` are - // relative roots - // - // TODO: public - internal var isAbsolute: Bool { - assert(FilePath(SystemString(self._slice)).root == self, "not a root") - - guard _windowsPaths else { return true } - - // `\` or `C:` are the only form of relative roots, and all - // absolute roots are at least 3 chars long. - let slice = self._slice - guard slice.count < 3 else { return true } - assert( - (slice.count == 1 && slice.first == .backslash) || - (slice.count == 2 && slice.last == .colon)) - return false - } -} - -@available(System 0.0.1, *) -extension FilePath { - internal var _portableDescription: String { - guard _windowsPaths else { return description } - let utf8 = description.utf8.map { $0 == UInt8(ascii: #"\"#) ? UInt8(ascii: "/") : $0 } - return String(decoding: utf8, as: UTF8.self) - } -} - -// Whether we are providing Windows paths -@inline(__always) -internal var _windowsPaths: Bool { - if let forceWindowsPaths = forceWindowsPaths { - return forceWindowsPaths - } - #if os(Windows) - return true - #else - return false - #endif -} - -@available(System 0.0.1, *) -extension FilePath { - // Whether we should add a separator when doing an append - internal var _needsSeparatorForAppend: Bool { - guard let last = _storage.last, !isSeparator(last) else { return false } - - // On Windows, we can have a path of the form `C:` which is a root and - // does not need a separator after it - if _windowsPaths && _relativeStart == _storage.endIndex { - return false - } - - return true - } - - // Perform an append, inseting a separator if needed. - // Note that this will not check whether `content` is a root - internal mutating func _append(unchecked content: Slice) { - assert(FilePath(SystemString(content)).root == nil) - if content.isEmpty { return } - if _needsSeparatorForAppend { - _storage.append(platformSeparator) - } - _storage.append(contentsOf: content) - } -} - -// MARK: - Invariants -@available(System 0.0.1, *) -extension FilePath { - internal func _invariantsSatisfied() -> Bool { - var normal = self - normal._normalizeSeparators() - guard self == normal else { return false } - guard !self._storage._hasTrailingSeparator() else { return false } - guard _hasRoot == (self.root != nil) else { return false } - return true - } - - internal func _invariantCheck() { - #if DEBUG - precondition(_invariantsSatisfied()) - #endif // DEBUG - } -} diff --git a/Sources/System/FilePath/FilePathString.swift b/Sources/System/FilePath/FilePathString.swift index 45f79c8a..98fff759 100644 --- a/Sources/System/FilePath/FilePathString.swift +++ b/Sources/System/FilePath/FilePathString.swift @@ -57,9 +57,11 @@ extension FilePath { @available(*, deprecated, message: "Use FilePath(_: String) to create a path from a String") public init(platformString: String) { if let nullLoc = platformString.firstIndex(of: "\0") { - self = FilePath(String(platformString[..? - var volume: Range? -} - -extension _ParsedWindowsRoot { - static func traditional( - drive: SystemChar?, fullQualified: Bool, endingAt idx: SystemString.Index - ) -> _ParsedWindowsRoot { - _ParsedWindowsRoot( - rootEnd: idx, - relativeBegin: idx, - drive: drive, - fullyQualified: fullQualified, - deviceSigil: nil, - host: nil, - volume: nil) - } - - static func unc( - deviceSigil: SystemChar?, - server: Range, - share: Range, - endingAt end: SystemString.Index, - relativeBegin relBegin: SystemString.Index - ) -> _ParsedWindowsRoot { - _ParsedWindowsRoot( - rootEnd: end, - relativeBegin: relBegin, - drive: nil, - fullyQualified: true, - deviceSigil: deviceSigil, - host: server, - volume: share) - } - - static func device( - deviceSigil: SystemChar, - volume: Range, - endingAt end: SystemString.Index, - relativeBegin relBegin: SystemString.Index - ) -> _ParsedWindowsRoot { - _ParsedWindowsRoot( - rootEnd: end, - relativeBegin: relBegin, - drive: nil, - fullyQualified: true, - deviceSigil: deviceSigil, - host: nil, - volume: volume) - } -} - -struct _Lexer { - var slice: Slice - - init(_ str: SystemString) { - self.slice = str[...] - } - - var backslash: SystemChar { .backslash } - - // Try to eat a backslash, returns false if nothing happened - mutating func eatBackslash() -> Bool { - slice._eat(.backslash) != nil - } - - // Try to consume a drive letter and subsequent `:`. - mutating func eatDrive() -> SystemChar? { - let copy = slice - if let d = slice._eat(if: { $0.isLetter }), slice._eat(.colon) != nil { - return d - } - // Restore slice - slice = copy - return nil - } - - // Try to consume a device sigil (stand-alone . or ?) - mutating func eatSigil() -> SystemChar? { - let copy = slice - guard let sigil = slice._eat(.question) ?? slice._eat(.dot) else { - return nil - } - - // Check for something like .hidden or ?question - guard isEmpty || slice.first == backslash else { - slice = copy - return nil - } - - return sigil - } - - // Try to consume an explicit "UNC" directory - mutating func eatUNC() -> Bool { - slice._eatSequence("UNC".unicodeScalars.lazy.map { SystemChar(ascii: $0) }) != nil - } - - // Eat everything up to but not including a backslash or null - mutating func eatComponent() -> Range { - let backslash = self.backslash - let component = slice._eatWhile({ $0 != backslash }) - ?? slice[slice.startIndex ..< slice.startIndex] - return component.indices - } - - var isEmpty: Bool { - return slice.isEmpty - } - - var current: SystemString.Index { slice.startIndex } - - mutating func clear() { - // TODO: Intern empty system string - self = _Lexer(SystemString()) - } - - mutating func reset(to: SystemString, at: SystemString.Index) { - self.slice = to[at...] - } -} - -internal struct WindowsRootInfo { - // The "volume" of a root. For UNC paths, this is also known as the "share". - internal enum Volume: Equatable { - /// No volume specified - /// - /// * Traditional root relative to the current drive: `\`, - /// * Omitted volume from other forms: `\\.\`, `\\.\UNC\server\\`, `\\server\\` - case empty - - // TODO: NT paths? Admin paths using `$`? - /// A specified drive. - /// - /// * Traditional disk: `C:\`, `C:` - /// * Device disk: `\\.\C:\`, `\\?\C:\` - /// * UNC: `\\server\e:\`, `\\?\UNC\server\e:\` - case drive(Character) - - // TODO: GUID type? - /// A volume with a GUID in a non-traditional path - /// - /// * UNC: `\\host\Volume{0000-...}\`, `\\.\UNC\host\Volume{0000-...}\` - /// * Device roots: `\\.\Volume{0000-...}\`, `\\?\Volume{000-...}\` - case guid(String) - - // TODO: Legacy DOS devices, such as COM1? - - /// Device object or share name - /// - /// * Device roots: `\\.\BootPartition\` - /// * UNC: `\\host\volume\` - case volume(String) - - // TODO: Should legacy DOS devices be detected and/or converted at construction time? - // TODO: What about NT paths: `\??\` - } - - /// Represents the syntactic form of the path - internal enum Form: Equatable { - /// Traditional DOS roots: `C:\`, `C:`, and `\` - case traditional(fullyQualified: Bool) // `C:\`, `C:`, `\` - - /// UNC syntactic form: `\\server\share\` - case unc - - /// DOS device syntactic form: `\\?\BootPartition`, `\\.\C:\`, `\\?\UNC\server\share` - case device(sigil: Character) - - // TODO: NT? - } - - /// The host for UNC paths, else `nil`. - internal var host: String? - - /// The specified volume (or UNC share) for the root - internal var volume: Volume - - /// The syntactic form the root is in - internal var form: Form - - init(host: String?, volume: Volume, form: Form) { - self.host = host - self.volume = volume - self.form = form - checkInvariants() - } -} - -extension _ParsedWindowsRoot { - fileprivate func volumeInfo(_ root: SystemString) -> WindowsRootInfo.Volume { - if let d = self.drive { - return .drive(Character(d.asciiScalar!)) - } - - guard let vol = self.volume, !vol.isEmpty else { return .empty } - - // TODO: check for GUID - // TODO: check for drive - return .volume(root[vol].string) - } -} - -extension WindowsRootInfo { - internal init(_ root: SystemString, _ parsed: _ParsedWindowsRoot) { - self.volume = parsed.volumeInfo(root) - - if let host = parsed.host { - self.host = root[host].string - } else { - self.host = nil - } - - if let sig = parsed.deviceSigil { - self.form = .device(sigil: Character(sig.asciiScalar!)) - } else if parsed.host != nil { - assert(parsed.volume != nil) - self.form = .unc - } else { - self.form = .traditional(fullyQualified: parsed.fullyQualified) - } - } -} - -extension WindowsRootInfo { - /// NOT `\foo\bar` nor `C:foo\bar` - internal var isFullyQualified: Bool { - return form != .traditional(fullyQualified: false) - } - - /// - /// `\\server\share\foo\bar.exe`, `\\.\UNC\server\share\foo\bar.exe` - internal var isUNC: Bool { - host != nil - } - - /// - /// `\foo\bar.exe` - internal var isTraditionalRooted: Bool { - form == .traditional(fullyQualified: false) && volume == .empty - } - - /// - /// `C:foo\bar.exe` - internal var isTraditionalDriveRelative: Bool { - switch (form, volume) { - case (.traditional(fullyQualified: false), .drive(_)): return true - default: return false - } - } - - // TODO: Should this be component? - func formPath() -> FilePath { - fatalError("Unimplemented") - } - - // static func traditional( - // drive: Character?, fullyQualified: Bool - // ) -> WindowsRootInfo { - // let vol: Volume - // if let d = Character { - // vol = .drive(d) - // } else { - // vol = .relative - // } - // - // return WindowsRootInfo( - // volume: .relative, form: .traditional(fullyQualified: false)) - // } - - internal func checkInvariants() { - switch form { - case .traditional(let qual): - precondition(host == nil) - switch volume { - case .empty: - precondition(!qual) - break - case .drive(_): break - default: preconditionFailure() - } - case .unc: - precondition(host != nil) - case .device(_): break - } - } - -} - -extension SystemString { - // TODO: Or, should I always inline this to remove some of the bookeeping? - private func _parseWindowsRootInternal() -> _ParsedWindowsRoot? { - assert(_windowsPaths) - - /* - Windows root: device or UNC or DOS - device: (`\\.` or `\\?`) `\` (drive or guid or UNC-link) - drive: letter `:` - guid: `Volume{` (hex-digit or `-`)* `}` - UNC-link: `UNC\` UNC-volume - UNC: `\\` UNC-volume - UNC-volume: server `\` share - DOS: fully-qualified or legacy-device or drive or `\` - full-qualified: drive `\` - - TODO: What is \\?\server1\e:\utilities\\filecomparer\ from the docs? - TODO: What about admin use of `$` instead of `:`? E.g. \\system07\C$\ - - NOTE: Legacy devices are not handled by System at a library level, but - are deferred to the relevant syscalls. - */ - - var lexer = _Lexer(self) - - // Helper to parse a UNC root - func parseUNC(deviceSigil: SystemChar?) -> _ParsedWindowsRoot { - let serverRange = lexer.eatComponent() - guard lexer.eatBackslash() else { - fatalError("expected normalized root to contain backslash") - } - let shareRange = lexer.eatComponent() - let rootEnd = lexer.current - _ = lexer.eatBackslash() - return .unc( - deviceSigil: deviceSigil, - server: serverRange, share: shareRange, - endingAt: rootEnd, relativeBegin: lexer.current) - } - - - // `C:` or `C:\` - if let d = lexer.eatDrive() { - // `C:\` - fully qualified - let fullyQualified = lexer.eatBackslash() - return .traditional( - drive: d, fullQualified: fullyQualified, endingAt: lexer.current) - } - - // `\` or else it's just a rootless relative path - guard lexer.eatBackslash() else { return nil } - - // `\\` or else it's just a current-drive rooted traditional path - guard lexer.eatBackslash() else { - return .traditional( - drive: nil, fullQualified: false, endingAt: lexer.current) - } - - // `\\.` or `\\?` (device paths) or else it's just UNC - guard let sigil = lexer.eatSigil() else { - return parseUNC(deviceSigil: nil) - } - _ = sigil // suppress warnings - - guard lexer.eatBackslash() else { - fatalError("expected normalized root to contain backslash") - } - - if lexer.eatUNC() { - guard lexer.eatBackslash() else { - fatalError("expected normalized root to contain backslash") - } - return parseUNC(deviceSigil: sigil) - } - - let device = lexer.eatComponent() - let rootEnd = lexer.current - _ = lexer.eatBackslash() - - return .device( - deviceSigil: sigil, volume: device, - endingAt: rootEnd, relativeBegin: lexer.current) - } - - @inline(never) - internal func _parseWindowsRoot() -> ( - rootEnd: SystemString.Index, relativeBegin: SystemString.Index - ) { - guard let parsed = _parseWindowsRootInternal() else { - return (startIndex, startIndex) - } - return (parsed.rootEnd, parsed.relativeBegin) - } -} - -extension SystemString { - // UNC and device roots can have multiple repeated roots that are meaningful, - // and extra backslashes may need to be inserted for partial roots (e.g. empty - // volume). - // - // Returns the point where `_normalizeSeparators` should resume. - internal mutating func _prenormalizeWindowsRoots() -> Index { - assert(_windowsPaths) - assert(!self.contains(.slash), "only valid after separator conversion") - - var lexer = _Lexer(self) - - // Only relevant for UNC or device paths - guard lexer.eatBackslash(), lexer.eatBackslash() else { - return lexer.current - } - - // Parse a backslash, inserting one if needed - func expectBackslash() { - if lexer.eatBackslash() { return } - - // A little gross, but we reset the lexer because the lexer - // holds a strong reference to `self`. - // - // TODO: Intern the empty SystemString. Right now, this is - // along an uncommon/pathological case, but we want to in - // general make empty strings without allocation - let idx = lexer.current - lexer.clear() - self.insert(.backslash, at: idx) - lexer.reset(to: self, at: idx) - let p = lexer.eatBackslash() - assert(p) - } - // Parse a component and subsequent backslash, insering one if needed - func expectComponent() { - _ = lexer.eatComponent() - expectBackslash() - } - - // Check for `\\.` style paths - if lexer.eatSigil() != nil { - expectBackslash() - if lexer.eatUNC() { - expectBackslash() - expectComponent() - expectComponent() - return lexer.current - } - expectComponent() - return lexer.current - } - - expectComponent() - expectComponent() - return lexer.current - } -} - -#if os(Windows) -import WinSDK - -// FIXME: Rather than canonicalizing the path at every call site to a Win32 API, -// we should consider always storing absolute paths with the \\?\ prefix applied, -// for better performance. -extension UnsafePointer where Pointee == CInterop.PlatformChar { - /// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee, - /// to ensure long paths greater than MAX_PATH (260) characters are handled correctly. - /// - /// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation - internal func withCanonicalPathRepresentation(_ body: (Self) throws -> Result) throws -> Result { - // 1. Normalize the path first. - // Contrary to the documentation, this works on long paths independently - // of the registry or process setting to enable long paths (but it will also - // not add the \\?\ prefix required by other functions under these conditions). - let dwLength: DWORD = GetFullPathNameW(self, 0, nil, nil) - return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in - guard (1.. DWORD { - DWORD(hr) & 0xffff -} - -@inline(__always) -fileprivate func HRESULT_FACILITY(_ hr: HRESULT) -> DWORD { - DWORD(hr >> 16) & 0x1fff -} - -@inline(__always) -fileprivate func SUCCEEDED(_ hr: HRESULT) -> Bool { - hr >= 0 -} - -// This is a non-standard extension to the Windows SDK that allows us to convert -// an HRESULT to a Win32 error code. -@inline(__always) -fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD { - if SUCCEEDED(hr) { return ERROR_SUCCESS } - if HRESULT_FACILITY(hr) == FACILITY_WIN32 { - return HRESULT_CODE(hr) - } - return DWORD(hr) -} -#endif diff --git a/Sources/System/FilePath/SystemStringShim.swift b/Sources/System/FilePath/SystemStringShim.swift new file mode 100644 index 00000000..156fddb9 --- /dev/null +++ b/Sources/System/FilePath/SystemStringShim.swift @@ -0,0 +1,79 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2026 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +// PORT SHIM: bidirectional converters between the old package substrate +// (SystemString/SystemChar) and the SE-0529 stdlib copy's substrate +// (_SystemString/FilePath.CodeUnit). +// +// SystemChar.RawValue is CInterop.PlatformChar and FilePath.CodeUnit is +// CChar on Unix / UInt16 on Windows: the same underlying type on every +// platform, so conversion is a per-element rewrap, O(n) copy. +// +// Temporary by design: once one substrate wins (long-term plan: converge +// on _SystemString), these converters and their call sites are the +// mechanical removal list. Do not grow API on top of them. + +@available(SwiftStdlib 9999, *) +extension SystemString { + /// Convert from the stdlib copy's string representation. + internal init(_ str: _SystemString) { + self.init( + nullTerminated: str.nullTerminatedStorage.map { SystemChar(rawValue: $0) } + ) + } +} + +@available(SwiftStdlib 9999, *) +extension _SystemString { + /// Convert from the old package string representation. + internal init(_ str: SystemString) { + self.init( + nullTerminated: str.nullTerminatedStorage.map { $0.rawValue } + ) + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// Construct from the old package string representation. + /// + /// Goes through the copy's `_normalizing` funnel so the result satisfies + /// the stdlib implementation's storage invariants (coalesced separators, + /// normalized dots). Note this can store a different byte spelling than + /// the old FilePath(SystemString) did. + internal init(_ str: SystemString) { + self.init(_normalizing: _SystemString(str)) + } + + /// View the copy's storage as the old package string representation. + /// O(n) copy; port-transition use only. + internal var _systemStringStorage: SystemString { + SystemString(_storage) + } +} + +@available(SwiftStdlib 9999, *) +extension _SystemString { + /// Calls `body` with a null-terminated platform-string view of the + /// contents. Mirrors the old `SystemString.withPlatformString`; + /// `FilePath.CodeUnit` and `CInterop.PlatformChar` are layout-identical + /// on every platform (CChar on Unix, UInt16 on Windows). + internal func withPlatformString( + _ f: (UnsafePointer) throws -> T + ) rethrows -> T { + try withNullTerminatedCodeUnits { units in + try units.baseAddress!.withMemoryRebound( + to: CInterop.PlatformChar.self, capacity: units.count + ) { pointer in + assert(pointer[self.count] == 0) + return try f(pointer) + } + } + } +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePath.swift b/Sources/System/StdlibFilePathImplementation/FilePath.swift new file mode 100644 index 00000000..245d875e --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePath.swift @@ -0,0 +1,171 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +/// A file path is a null-terminated sequence of bytes that represents +/// a location in the file system. +@available(SwiftStdlib 9999, *) +public struct FilePath: Sendable { + internal var _storage: _SystemString + + /// Creates an empty file path. + @available(SwiftStdlib 9999, *) + public init() { + self._storage = _SystemString() + } + + internal init(_storage: _SystemString) { + self._storage = _storage + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath { + // Normalizing init: the funnel for all path construction. + // + // All three platforms coalesce separators first, then parse. Darwin + // additionally canonicalizes the anchor and excludes the resource-fork + // suffix from dot-normalization (see _normalizeDarwin). + internal init(_normalizing str: _SystemString) { + if _isDarwin { + self = _normalizeDarwin(str) + } else if _isWindows { + self = _normalizeWindows(str) + } else { + self = _normalizeLinux(str) + } + } + + /// The platform's canonical directory separator, as a code unit. + /// + /// On Linux and Darwin, this is the code unit for `/`. + /// On Windows, it is the code unit for `\`. + @available(SwiftStdlib 9999, *) + public static var separator: FilePath.CodeUnit { + _platformSeparator + } + + /// Whether this path is empty. + @available(SwiftStdlib 9999, *) + public var isEmpty: Bool { _storage.isEmpty } +} + +// MARK: - Per-platform normalization + +@available(SwiftStdlib 9999, *) +private func _normalizeLinux(_ str: _SystemString) -> FilePath { + _internalInvariant(_isLinux) + var s = str + s._normalizeSeparators() + let (rootEnd, relBegin) = s._parseRoot() + let isRooted = rootEnd != s.startIndex + var result = _SystemString() + result.append(contentsOf: s[s.startIndex.. FilePath { + var s = str + s._normalizeSeparators() + let isVerbatim = _isVerbatimComponentPath(s) + let (rootEnd, relBegin) = s._parseRoot() + // The only non-rooted Windows anchor is the 2-byte drive-relative `C:`; + // empty/no-root counts as not rooted. Every other anchor (`\`, `C:\`, + // UNC, verbatim, …) is rooted. + let isRooted = rootEnd != s.startIndex + && !_isDriveRelativeAnchor(s[s.startIndex.. FilePath { + // Coalesce separators across the whole string first, then canonicalize + // the anchor and parse the anchor / resource-fork suffix boundaries on + // those coalesced bytes the way XNU classifies them. + // + // This is deliberately *not* what XNU does byte-for-byte: the kernel + // does not coalesce separators before recognizing the + // .vol/.resolve/.nofollow anchors. Because we coalesce first, our + // canonicalization is XNU's modulo separator-coalescing — spellings + // that differ only in runs of separators store identically. e.g. + // /.resolve//1/foo and /.resolve/1/foo both coalesce and canonicalize + // to /.nofollow/foo. + var s = str + s._normalizeSeparators() + s._canonicalizeDarwinAnchor() + + // Parse the anchor and resource-fork suffix on the + // coalesced+canonicalized string. + let (rootEnd, relBegin) = s._parseRoot() + let hasAnchor = rootEnd != s.startIndex + var suffixStart = s._resourceForkSuffixStart ?? s.endIndex + // If the suffix overlaps the anchor region, it's not a real suffix. + if suffixStart < relBegin { + suffixStart = s.endIndex + } + + // TODO(post-PR): Single pass instead of both `s` and `result` copies. + + // Reassemble: anchor + gap + dot-normalized relative + suffix — + // appending the relative portion into `result` + var result = _SystemString() + result.append(contentsOf: s[.. Bool { + guard _isWindows else { return false } + guard let parsed = storage._parseWindowsRootInternal() else { return false } + return parsed.isVerbatimComponent +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePathAnchor.swift b/Sources/System/StdlibFilePathImplementation/FilePathAnchor.swift new file mode 100644 index 00000000..c9a7a958 --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathAnchor.swift @@ -0,0 +1,192 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// The anchor of a file path identifies a reference point + /// and precedes any components. + @available(SwiftStdlib 9999, *) + public struct Anchor: Sendable { + internal var _path: FilePath + internal var _end: _SystemString.Index + + internal init(_path: FilePath, _end: _SystemString.Index) { + self._path = _path + self._end = _end + } + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Anchor { + internal var _slice: _SystemString.SubSequence { + _internalInvariant(_end >= _path._storage.startIndex && _end <= _path._storage.endIndex) + return _path._storage[_path._storage.startIndex..<_end] + } + + /// Whether this anchor is rooted. + @available(SwiftStdlib 9999, *) + public var isRooted: Bool { + guard _isWindows else { return true } + + // On Windows, the only non-rooted anchor is drive-relative `C:` + // (relative to the CWD on that drive). Everything else — `\`, + // `C:\`, `\\server\share`, `\\?\...` — is rooted. + return !_isDriveRelativeAnchor(_slice) + } +} + +#if os(Windows) +@available(SwiftStdlib 9999, *) +extension FilePath.Anchor { + /// The drive letter of this anchor, if any. + /// + /// Returns the single code unit preceding the colon for drive-style + /// anchors (`C:\`, `C:`, `\\?\C:\`, `\\.\C:\`), and `nil` for UNC + /// anchors, non-drive device anchors, and the current-drive root `\`. + /// + /// The value is presented as written, without case normalization. + /// If the drive letter is an unpaired surrogate, `U+FFFD` is returned. + @available(SwiftStdlib 9999, *) + public var driveLetter: Unicode.Scalar? { + _parseWindowsAnchor()?.drive?._driveLetterScalar + } + + /// Whether this anchor uses the Windows verbatim-component form. + @available(SwiftStdlib 9999, *) + public var isVerbatimComponent: Bool { + guard let parsed = _parseWindowsAnchor() else { return false } + return parsed.isVerbatimComponent + } + + private func _parseWindowsAnchor() -> _ParsedWindowsRoot? { + _path._storage._parseWindowsRootInternal() + } +} +#endif + +// MARK: - Anchor Hashable, Comparable, descriptions + +@available(SwiftStdlib 9999, *) +extension FilePath.Anchor: Hashable { + @available(SwiftStdlib 9999, *) + public static func == (lhs: FilePath.Anchor, rhs: FilePath.Anchor) -> Bool { + lhs._slice.elementsEqual(rhs._slice) + } + @available(SwiftStdlib 9999, *) + public func hash(into hasher: inout Hasher) { + for c in _slice { + hasher.combine(c) + } + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Anchor: Comparable { + @available(SwiftStdlib 9999, *) + public static func < (lhs: FilePath.Anchor, rhs: FilePath.Anchor) -> Bool { + lhs._slice.lexicographicallyPrecedes(rhs._slice) + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Anchor: CustomStringConvertible, CustomDebugStringConvertible { + @available(SwiftStdlib 9999, *) + public var description: String { + unsafe _slice.withCodeUnits { + unsafe $0.withMemoryRebound(to: FilePath._Encoding.CodeUnit.self) { + unsafe String(decoding: $0, as: FilePath._Encoding.self) + } + } + } + @available(SwiftStdlib 9999, *) + public var debugDescription: String { + description.debugDescription + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Anchor: ExpressibleByStringLiteral { + /// Creates an anchor from a string literal. + /// + /// Precondition: the literal is non-empty, contains no `NUL`, + /// and forms a valid anchor. + @available(SwiftStdlib 9999, *) + public init(stringLiteral: String) { + guard let a = FilePath.Anchor(stringLiteral) else { + fatalError( + "FilePath.Anchor string literal must be non-empty," + + " must not contain NUL, and must form a valid anchor") + } + self = a + } + + /// Creates an anchor from a string. + /// + /// Returns `nil` if `string` is empty, contains `NUL`, or is + /// not a valid anchor. + @available(SwiftStdlib 9999, *) + public init?(_ string: String) { + guard let path = FilePath(string) else { return nil } + guard let anchor = path.anchor else { return nil } + guard path.components.isEmpty && !path.hasTrailingSeparator else { + return nil + } + // A named anchor form must carry its name. `FilePath.init?` is total + // and coalesces the degenerate Windows roots — incomplete UNC (`\\`, + // `\\server`), empty device (`\\.`/`\\.\`), and empty verbatim + // (`\\?`/`\\?\`) — into a degraded anchor, but as a typed `Anchor` + // value they name a volume/device/share that isn't there, so the + // failable `Anchor` initializer rejects them. This strictness lives + // here, in anchor validation only; `FilePath` decomposition of these + // inputs is unchanged. + if _isWindows && _isIncompleteWindowsNamedAnchor(anchor._slice) { + return nil + } + self = anchor + } +} + +/// Returns `true` when `anchorBytes` is a Windows UNC/device/verbatim +/// anchor form that is missing its name: incomplete UNC (`\\`, `\\server`), +/// empty device (`\\.\`), or empty verbatim (`\\?\`). +/// +/// This is the strictness predicate for `FilePath.Anchor.init?`. The walk is +/// local to anchor validation and shares nothing with the construction +/// parser (`_parseWindowsRootInternal` and friends), which must keep +/// coalescing these forms unchanged for `FilePath`. +/// +/// A named form requires its name: UNC needs a non-empty server AND a +/// non-empty share; device (`\\.\`) needs a non-empty device name; verbatim +/// (`\\?\`) needs a non-empty component after the prefix. Traditional roots +/// (`\`, `C:`, `C:\`) carry no separate name and are never rejected here. +@available(SwiftStdlib 9999, *) +private func _isIncompleteWindowsNamedAnchor( + _ anchorBytes: Slice<_SystemString> +) -> Bool { + // A named form begins with the two-backslash UNC/device/verbatim prefix. + // One leading `\` is the bare current-drive root, and `C:` / `C:\` carry a + // drive; none of those are a name-bearing form with the name missing. + var s = anchorBytes + guard s._eat(._backslash) != nil, s._eat(._backslash) != nil else { + return false + } + // Device (`\\.\`) or verbatim (`\\?\`). Separator + // coalescing always stores the prefix backslash (`\\.\` / `\\?\`), so + // whatever follows it is the name; an empty name means incomplete. + if s._eat(if: { $0 == ._dot || $0 == ._question }) != nil { + guard s._eat(._backslash) != nil else { return true } + return s.isEmpty + } + // UNC (`\\server\share`): require a non-empty server AND a non-empty share. + guard s._eatWhile({ $0 != ._backslash }) != nil else { return true } + guard s._eat(._backslash) != nil else { return true } + return s.isEmpty +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePathCodeUnits.swift b/Sources/System/StdlibFilePathImplementation/FilePathCodeUnits.swift new file mode 100644 index 00000000..5c98eba5 --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathCodeUnits.swift @@ -0,0 +1,151 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +// MARK: - CodeUnit typealias + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// The type used to represent a "character" in the platform's + /// native path encoding. + #if os(Windows) + @available(SwiftStdlib 9999, *) + public typealias CodeUnit = UInt16 + #else + @available(SwiftStdlib 9999, *) + public typealias CodeUnit = CChar + #endif + + /// The Unicode encoding corresponding to `CodeUnit`. + #if os(Windows) + internal typealias _Encoding = UTF16 + #else + internal typealias _Encoding = UTF8 + #endif +} + +// MARK: - withCodeUnits (C interop) + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// Calls the given closure with a pointer to the path's null-terminated + /// contents and the number of code units preceding the null terminator. + /// The pointer is valid only for the duration of the closure, and the + /// count does not include the null terminator. + /// + /// On Windows the pointer is wide (`UnsafePointer`); see + /// also `String.withCString(encodedAs:_:)`. + @available(SwiftStdlib 9999, *) + public func withCodeUnits( + _ body: (UnsafePointer, Int) throws(E) -> Result + ) throws(E) -> Result { + // Storage is already [FilePath.CodeUnit] with a trailing null, so + // we can just hand out its base address and the length sans null. + let storage = _storage.nullTerminatedStorage + let count = storage.count - 1 + return try unsafe storage.withUnsafeBufferPointer { buf throws(E) in + try unsafe body(buf.baseAddress!, count) + } + } +} + +// MARK: - Code unit access (Span) + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// A span of the platform code units comprising this path, not + /// including the null terminator. + @available(SwiftStdlib 9999, *) + public var codeUnits: Span { + _storage._span + } + + /// A span of the platform code units comprising this path, including + /// the trailing null terminator as its final element. + @available(SwiftStdlib 9999, *) + public var nullTerminatedCodeUnits: Span { + _storage._nullTerminatedSpan + } + + /// Creates a file path from a span of platform code units. + /// + /// The span should not include a null terminator. Returns `nil` + /// if the span contains `NUL`, which is not a valid path byte + /// on any supported platform. + @available(SwiftStdlib 9999, *) + public init?(codeUnits: Span) { + var chars = [FilePath.CodeUnit]() + chars.reserveCapacity(codeUnits.count + 1) + for i in codeUnits.indices { + let c = codeUnits[i] + guard c != ._null else { return nil } + chars.append(c) + } + chars.append(._null) + let str = _SystemString(nullTerminated: chars) + self.init(_normalizing: str) + } + + // TODO: Add the init + + // NOTE: The proposal specifies an OutputSpan-based initializer: + // + // public init( + // capacity: Int, + // initializingCodeUnitsWith initializer: + // (inout OutputSpan) throws(E) -> Void + // ) throws(E) + // + // OutputSpan requires experimental features not available without + // compiler flags. Stubbed until OutputSpan is generally available. +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Component { + /// A span of the platform code units comprising this component. + @available(SwiftStdlib 9999, *) + public var codeUnits: Span { + _path._storage._nullTerminatedSpan.extracting(_range) + } + + /// Creates a file path component from a span of platform code units. + /// + /// Returns `nil` if the code units are empty, contain `NUL`, or are + /// otherwise invalid (e.g. contain more than one component). + @available(SwiftStdlib 9999, *) + public init?(codeUnits: Span) { + guard !codeUnits.isEmpty else { return nil } + guard let path = FilePath(codeUnits: codeUnits) else { return nil } + self.init(_validating: path) + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Anchor { + /// A span of the platform code units comprising this anchor. + @available(SwiftStdlib 9999, *) + public var codeUnits: Span { + _path._storage._nullTerminatedSpan.extracting( + _path._storage.startIndex..<_end) + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.ComponentView { + /// A span of the platform code units comprising the relative + /// components portion of the path. + @available(SwiftStdlib 9999, *) + public var codeUnits: Span { + // The component view spans `[_relStart, _relEnd)`. By construction + // `_relEnd` excludes any structural suffix (trailing separator on the + // relative region, or a Darwin resource-fork suffix), so this range + // is exactly the components-region bytes. + _path._storage._nullTerminatedSpan.extracting(_relStart..<_relEnd) + } +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePathComponent.swift b/Sources/System/StdlibFilePathImplementation/FilePathComponent.swift new file mode 100644 index 00000000..5aa3ea9e --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathComponent.swift @@ -0,0 +1,137 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// Represents an individual component of a file path. + @available(SwiftStdlib 9999, *) + public struct Component: Sendable { + internal var _path: FilePath + internal var _range: Range<_SystemString.Index> + internal var _verbatimContext: Bool + + internal init(_path: FilePath, _range: Range<_SystemString.Index>, _verbatimContext: Bool) { + self._path = _path + self._range = _range + self._verbatimContext = _verbatimContext + } + + internal var _slice: _SystemString.SubSequence { + _internalInvariant(_range.lowerBound >= _path._storage.startIndex) + _internalInvariant(_range.upperBound <= _path._storage.endIndex) + return _path._storage[_range] + } + + /// Whether a component is a regular file or directory name, or a special + /// directory `.` or `..` + @available(SwiftStdlib 9999, *) + public enum Kind: Sendable, Equatable { + case currentDirectory + case parentDirectory + case regular + } + + /// The kind of this component. + @available(SwiftStdlib 9999, *) + public var kind: Kind { + if _verbatimContext { return .regular } + let s = _slice + if s.elementsEqual([._dot]) { return .currentDirectory } + if s.elementsEqual([._dot, ._dot]) { return .parentDirectory } + return .regular + } + } +} + +// MARK: - Component Hashable, Comparable, descriptions + +@available(SwiftStdlib 9999, *) +extension FilePath.Component: Hashable { + @available(SwiftStdlib 9999, *) + public static func == (lhs: FilePath.Component, rhs: FilePath.Component) -> Bool { + lhs._slice.elementsEqual(rhs._slice) + } + @available(SwiftStdlib 9999, *) + public func hash(into hasher: inout Hasher) { + for c in _slice { + hasher.combine(c) + } + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Component: Comparable { + @available(SwiftStdlib 9999, *) + public static func < (lhs: FilePath.Component, rhs: FilePath.Component) -> Bool { + lhs._slice.lexicographicallyPrecedes(rhs._slice) + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Component: CustomStringConvertible, CustomDebugStringConvertible { + @available(SwiftStdlib 9999, *) + public var description: String { + unsafe _slice.withCodeUnits { + unsafe $0.withMemoryRebound(to: FilePath._Encoding.CodeUnit.self) { + unsafe String(decoding: $0, as: FilePath._Encoding.self) + } + } + } + @available(SwiftStdlib 9999, *) + public var debugDescription: String { + description.debugDescription + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Component: ExpressibleByStringLiteral { + /// Creates a file path component from a string literal. + /// + /// Precondition: `stringLiteral` is non-empty and contains no `NUL` + /// or directory separator. + @available(SwiftStdlib 9999, *) + public init(stringLiteral: String) { + guard let c = FilePath.Component(stringLiteral) else { + fatalError( + "FilePath.Component string literal must be non-empty" + + " and must not contain NUL or a directory separator") + } + self = c + } + + /// Creates a file path component from a string. + /// + /// Returns `nil` if `string` is empty or contains `NUL` or a + /// directory separator. + @available(SwiftStdlib 9999, *) + public init?(_ string: String) { + guard !string.isEmpty else { return nil } + guard let path = FilePath(string) else { return nil } + self.init(_validating: path) + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.Component { + /// Shared validation behind `init?(_:)` and `init?(codeUnits:)`. + /// + /// Succeeds only when `path` is exactly one component with no anchor and + /// no trailing separator — i.e. a bare component with no embedded *or* + /// trailing directory separator, matching the contract that a component + /// contains no separators. So `a/b` (interior) and `a/` (trailing) are + /// both rejected, as is any anchored input. NUL-rejection has already + /// happened in `FilePath.init?`; callers funnel through one of those. + internal init?(_validating path: FilePath) { + guard path.anchor == nil, !path.hasTrailingSeparator else { return nil } + let comps = path.components + guard comps.count == 1 else { return nil } + self = comps.first! + } +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePathComponentView.swift b/Sources/System/StdlibFilePathImplementation/FilePathComponentView.swift new file mode 100644 index 00000000..9559a828 --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathComponentView.swift @@ -0,0 +1,312 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// A bidirectional, range-replaceable collection of the + /// components that make up a file path. + @available(SwiftStdlib 9999, *) + public struct ComponentView: Sendable { + internal var _path: FilePath + + // Start of this view's contribution in _path._storage. Set at + // creation (= source path's rootEnd at that moment), immutable + // through mutations. Used by the splice in `FilePath.components`'s + // `set` to copy this view's bytes into a target. _relStart may move + // forward as absorption shifts the re-parsed anchor; _originalStart + // does not. + internal let _originalStart: _SystemString.Index + + // Recomputed after each `replaceSubrange`. Used for iteration. + internal var _relStart: _SystemString.Index // first byte of components + internal var _relEnd: _SystemString.Index // start of suffix (or storage end) + internal var _suffixEnd: _SystemString.Index // end of storage + + internal init(_path: FilePath) { + self._path = _path + let (rootEnd, relBegin) = _path._storage._parseRoot() + self._originalStart = rootEnd + self._relStart = relBegin + self._relEnd = _path._storage._componentViewRelEnd(relBegin: relBegin) + self._suffixEnd = _path._storage.endIndex + + _internalInvariant(_originalStart <= _relStart) + _internalInvariant(_relStart <= _relEnd) + _internalInvariant(_relEnd <= _suffixEnd) + _internalInvariant(_suffixEnd == _path._storage.endIndex) + } + } +} + +// MARK: - Index + +@available(SwiftStdlib 9999, *) +extension FilePath.ComponentView { + @available(SwiftStdlib 9999, *) + public struct Index: Sendable, Comparable, Hashable { + internal var _storage: _SystemString.Index + + @available(SwiftStdlib 9999, *) + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs._storage < rhs._storage + } + + internal init(_storage: _SystemString.Index) { + self._storage = _storage + } + } +} + +// MARK: - Internal helpers + +@available(SwiftStdlib 9999, *) +extension _SystemString { + /// The end of the iterable component region for `ComponentView`. + /// + /// Excludes structural suffixes that live in `[_relEnd, _suffixEnd)`: + /// a Darwin resource-fork suffix (the leading `/` of `/..namedfork/rsrc`) + /// or a trailing separator on the relative region. Returns `endIndex` + /// when there is no such suffix. + /// + /// The `relBegin` guard for the trailing separator is essential: for + /// paths like `/`, `\\server\share\`, `C:\`, the trailing byte of + /// storage IS a separator but it belongs to the anchor/gap, not the + /// relative region. For the resource-fork case, an overlap with the + /// anchor region (`/..namedfork/rsrc` → `rsrcStart < relBegin`) means + /// there are no relative components at all. + internal func _componentViewRelEnd( + relBegin: Index + ) -> Index { + if _isDarwin, let rsrcStart = _resourceForkSuffixStart { + return rsrcStart >= relBegin ? rsrcStart : relBegin + } + if !isEmpty { + let lastIdx = index(before: endIndex) + if _isSeparator(self[lastIdx]) && lastIdx >= relBegin { + return lastIdx + } + } + return endIndex + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.ComponentView { + // Re-derive _relStart/_relEnd/_suffixEnd from current _path._storage. + // _originalStart is intentionally NOT touched — it's set at view + // creation and stays put even when re-decomposition shifts the anchor. + internal mutating func _recomputeIndices() { + let (_, relBegin) = _path._storage._parseRoot() + _relStart = relBegin + _relEnd = _path._storage._componentViewRelEnd(relBegin: relBegin) + _suffixEnd = _path._storage.endIndex + } + + internal func _componentEnd(at pos: _SystemString.Index) -> _SystemString.Index { + var i = pos + while i < _relEnd && !_isSeparator(_path._storage[i]) { + _path._storage.formIndex(after: &i) + } + return i + } + + internal func _skipSeparators(from pos: _SystemString.Index) -> _SystemString.Index { + var i = pos + while i < _relEnd && _isSeparator(_path._storage[i]) { + _path._storage.formIndex(after: &i) + } + return i + } +} + +// MARK: - BidirectionalCollection + +@available(SwiftStdlib 9999, *) +extension FilePath.ComponentView: BidirectionalCollection { + @available(SwiftStdlib 9999, *) + public typealias Element = FilePath.Component + + @available(SwiftStdlib 9999, *) + public var startIndex: Index { + // Skip gap separator(s) between anchor and first component + Index(_storage: _skipSeparators(from: _relStart)) + } + + @available(SwiftStdlib 9999, *) + public var endIndex: Index { + // endIndex is the end of the iterable (component) region — the start + // of any structural suffix (a trailing separator on the relative + // region, or a Darwin resource-fork suffix), or end of storage if + // there is no such suffix. + Index(_storage: _relEnd) + } + + @available(SwiftStdlib 9999, *) + public var isEmpty: Bool { + startIndex == endIndex + } + + @available(SwiftStdlib 9999, *) + public func index(after i: Index) -> Index { + let compEnd = _componentEnd(at: i._storage) + let next = _skipSeparators(from: compEnd) + return Index(_storage: next) + } + + @available(SwiftStdlib 9999, *) + public func index(before i: Index) -> Index { + var idx = i._storage + // Back up past separator(s) + while idx > startIndex._storage + && _isSeparator(_path._storage[_path._storage.index(before: idx)]) { + _path._storage.formIndex(before: &idx) + } + // Back up past component bytes + while idx > startIndex._storage + && !_isSeparator(_path._storage[_path._storage.index(before: idx)]) { + _path._storage.formIndex(before: &idx) + } + return Index(_storage: idx) + } + + @available(SwiftStdlib 9999, *) + public subscript(position: Index) -> FilePath.Component { + let end = _componentEnd(at: position._storage) + _internalInvariant(end > position._storage, "Component must be non-empty") + let isVerbatim = _isVerbatimComponentPath(_path._storage) + return FilePath.Component( + _path: _path, _range: position._storage..( + _ subrange: Range, with newElements: C + ) where C: Collection, C.Element == FilePath.Component { + let touchesEnd = subrange.upperBound == endIndex && + !(subrange.isEmpty && newElements.isEmpty) + + // Compute byte range to splice. When the subrange touches endIndex, + // we extend to _suffixEnd (end of storage including any suffix + // bytes), NOT just to _relEnd. RRC operations that touch the end + // affect the entire suffix region — `removeLast` strips a trailing + // separator OR a resource fork; `append` replaces the suffix bytes + // with the new component. + let byteLower = subrange.lowerBound._storage + let byteUpper = touchesEnd ? _suffixEnd : subrange.upperBound._storage + + if newElements.isEmpty { + // Indices point to component starts. The range [byteLower, byteUpper) + // covers the removed component(s) plus the joining separator that + // follows them. The exception is `touchesEnd`: there is no following + // component to join to, so we instead back `adjLower` over the + // PRECEDING separator to keep the result well-formed (no dangling + // sep after the last surviving component). + var adjLower = byteLower + if touchesEnd && adjLower > _relStart + && _isSeparator(_path._storage[_path._storage.index(before: adjLower)]) { + _path._storage.formIndex(before: &adjLower) + } + _path._storage.removeSubrange(adjLower.. _relStart { + needLeadingSep = !_isSeparator( + _path._storage[_path._storage.index(before: byteLower)]) + } else if _path._storage.startIndex < _relStart { + // Inserting at the start of the relative region. Add a gap + // separator if the anchor (plus any existing gap sep up to + // _relStart) needs one before component bytes. + needLeadingSep = _anchorNeedsGapSeparator(_path._storage[..<_relStart]) + } else { + needLeadingSep = false + } + + if touchesEnd { + // The splice runs to end-of-storage. Truncate, then append — + // no intermediary, no inserts. There's no trailing sep in + // this case (touchesEnd implies the splice consumes everything + // up to and including any existing suffix bytes). + _path._storage.removeSubrange(byteLower.. 0 { _path._storage.append(_platformSeparator) } + _path._storage.append(contentsOf: comp._slice) + } + } else { + // Middle splice — there's a tail after byteUpper that has to + // stay put. We need a single replaceSubrange to keep the + // tail's index arithmetic straight, which means an intermediary. + let needTrailingSep = + byteUpper < _relEnd && !_isSeparator(_path._storage[byteUpper]) + + var bytes = _SystemString() + if needLeadingSep { bytes.append(_platformSeparator) } + for (i, comp) in newElements.enumerated() { + if i > 0 { bytes.append(_platformSeparator) } + bytes.append(contentsOf: comp._slice) + } + if needTrailingSep { bytes.append(_platformSeparator) } + + _path._storage.replaceSubrange(byteLower.. Bool { + lhs.elementsEqual(rhs) + } + @available(SwiftStdlib 9999, *) + public func hash(into hasher: inout Hasher) { + for c in self { + hasher.combine(c) + } + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath.ComponentView: Comparable { + @available(SwiftStdlib 9999, *) + public static func < (lhs: FilePath.ComponentView, rhs: FilePath.ComponentView) -> Bool { + for (l, r) in zip(lhs, rhs) { + if l < r { return true } + if r < l { return false } + } + return lhs.count < rhs.count + } +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePathDarwin.swift b/Sources/System/StdlibFilePathImplementation/FilePathDarwin.swift new file mode 100644 index 00000000..eb04c8d2 --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathDarwin.swift @@ -0,0 +1,177 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +// MARK: - Darwin anchor parsing + +// Darwin extends the basic Unix root `/` with: +// - Resolve flags: /.nofollow/, /.resolve/N/ +// - Volume references: /.vol/FSID/FILEID + +@available(SwiftStdlib 9999, *) +internal struct _ParsedDarwinAnchor { + var anchorEnd: _SystemString.Index + var relativeBegin: _SystemString.Index +} + +@available(SwiftStdlib 9999, *) +extension _SystemString { + // Try to parse a Darwin-specific anchor. + // Returns nil if this is just a plain `/` root. + internal func _parseDarwinAnchor() -> _ParsedDarwinAnchor? { + _internalInvariant(_isDarwin) + guard !isEmpty, self.first == ._slash else { return nil } + + let afterSlash = index(after: startIndex) + guard afterSlash < endIndex, self[afterSlash] == ._dot else { return nil } + + // Anchor grammar: /(flag)?(vol)? — flag is .nofollow/ or .resolve/N/, + // vol is .vol/FSID/FILEID(/)?, at least one of the two must be present. + let flag = _parseNofollow(from: afterSlash) ?? _parseResolve(from: afterSlash) + let volStart = flag?.relativeBegin ?? afterSlash + return _parseVol(from: volStart) ?? flag + } + + // MARK: - /.nofollow/ + + private func _parseNofollow(from dotIdx: Index) -> _ParsedDarwinAnchor? { + var s = self[dotIdx...] + guard s._eatSequence(".nofollow/"._asciiBytes) != nil else { return nil } + return _ParsedDarwinAnchor( + anchorEnd: s.startIndex, relativeBegin: s.startIndex) + } + + // MARK: - /.resolve// + + private func _parseResolve(from dotIdx: Index) -> _ParsedDarwinAnchor? { + var s = self[dotIdx...] + guard s._eatSequence(".resolve/"._asciiBytes) != nil, + s._eatWhile({ $0 != ._slash }) != nil, + s._eat(._slash) != nil else { return nil } + return _ParsedDarwinAnchor( + anchorEnd: s.startIndex, relativeBegin: s.startIndex) + } + + // MARK: - /.vol/FSID/FILEID + + private func _parseVol(from dotIdx: Index) -> _ParsedDarwinAnchor? { + guard let body = _parseVolBody(from: dotIdx) else { return nil } + return _ParsedDarwinAnchor( + anchorEnd: body.fileidRange.upperBound, + relativeBegin: body.relativeBegin) + } + + // Shared parser for the `.vol/FSID/FILEID[/]` body. Returns the + // FILEID range and the relative-portion start (past the trailing + // `/` if any). + private func _parseVolBody( + from dotIdx: Index + ) -> (fileidRange: Range, relativeBegin: Index)? { + var s = self[dotIdx...] + guard s._eatSequence(".vol/"._asciiBytes) != nil, + s._eatWhile({ $0 != ._slash }) != nil, + s._eat(._slash) != nil else { return nil } + let fileidStart = s.startIndex + guard s._eatWhile({ $0 != ._slash }) != nil else { return nil } + let fileidEnd = s.startIndex + let relBegin = s._eat(._slash) != nil ? s.startIndex : fileidEnd + return (fileidStart.. /.nofollow/ + // /.vol/NNNN/2/ -> /.vol/NNNN/@/ + // /.nofollow/.vol/NNNN/2/ -> /.nofollow/.vol/NNNN/@/ + // /.resolve/1/.vol/NNNN/2/ -> /.nofollow/.vol/NNNN/@/ + // /.resolve/3/.vol/NNNN/2/ -> /.resolve/3/.vol/NNNN/@/ + // + // Both replacements are independent and may both fire on a single + // combined anchor, so we don't return between them. + internal mutating func _canonicalizeDarwinAnchor() { + guard _isDarwin else { return } + + // /.resolve/1/ (12 bytes) -> /.nofollow/ (11 bytes). Storage shrinks + // by one byte; any trailing .vol portion shifts accordingly. Step 2 + // below re-finds positions on the post-replacement storage, so the + // shift is not load-bearing for callers. + let resolveOne = "/.resolve/1/" + if self.starts(with: resolveOne._asciiBytes) { + let prefixEnd = self.index(startIndex, offsetBy: resolveOne.utf8.count) + self.replaceSubrange( + startIndex.. .vol/FSID/@. The vol portion may be at the + // start of storage (bare) or right after a leading nofollow/resolve + // flag (combined anchor). + guard let volDot = _findAnchorVolDotPosition(), + let body = _parseVolBody(from: volDot) + else { return } + let fileidSlice = self[body.fileidRange] + if fileidSlice.count == 1 + && fileidSlice.first == FilePath.CodeUnit(_ascii: "2") { + self.replaceSubrange(body.fileidRange, with: CollectionOfOne(._at)) + } + } + + // Returns the index of the leading `.` of `.vol/...` within the + // anchor, if present. Three valid positions: + // - at startIndex+1, when storage starts with `/.vol/` + // - just past a leading `/.nofollow/` + // - just past a leading `/.resolve//` + // Returns nil if the anchor has no `.vol/...` portion. + private func _findAnchorVolDotPosition() -> Index? { + // Bare /.vol/ — the `.` is at startIndex+1. + if self.starts(with: "/.vol/"._asciiBytes) { + return self.index(after: startIndex) + } + // /.nofollow/.vol/ — the `.` of `.vol` is right after `/.nofollow/`. + var s = self[...] + if s._eatSequence("/.nofollow/"._asciiBytes) != nil, + self[s.startIndex...].starts(with: ".vol"._asciiBytes) { + return s.startIndex + } + // /.resolve//.vol/ — variable-length value. + s = self[...] + if s._eatSequence("/.resolve/"._asciiBytes) != nil, + s._eatWhile({ $0 != ._slash }) != nil, + s._eat(._slash) != nil, + self[s.startIndex...].starts(with: ".vol"._asciiBytes) { + return s.startIndex + } + return nil + } +} + +// MARK: - Resource fork detection + +@available(SwiftStdlib 9999, *) +extension _SystemString { + // The resource fork suffix is exactly "/..namedfork/rsrc" (17 bytes) + internal static var _resourceForkSuffix: String { "/..namedfork/rsrc" } + + internal var _resourceForkSuffixStart: _SystemString.Index? { + guard _isDarwin else { return nil } + let suffix = Self._resourceForkSuffix + let suffixCount = suffix.utf8.count + guard self.count >= suffixCount else { return nil } + let suffixStart = self.index(endIndex, offsetBy: -suffixCount) + return self[suffixStart...].elementsEqual(suffix._asciiBytes) + ? suffixStart : nil + } + + internal func _hasResourceForkSuffix() -> Bool { + _resourceForkSuffixStart != nil + } +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePathDecomposition.swift b/Sources/System/StdlibFilePathImplementation/FilePathDecomposition.swift new file mode 100644 index 00000000..7e799f72 --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathDecomposition.swift @@ -0,0 +1,274 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +// MARK: - Anchor property + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// The anchor of this path, if any. + @available(SwiftStdlib 9999, *) + public var anchor: Anchor? { + get { + let (rootEnd, _) = _storage._parseRoot() + guard rootEnd != _storage.startIndex else { return nil } + _internalInvariant(rootEnd <= _storage.endIndex) + return Anchor(_path: self, _end: rootEnd) + } + set { + let (rootEnd, relBegin) = _storage._parseRoot() + _internalInvariant(relBegin >= rootEnd) + if let newAnchor = newValue { + // Replace old root region (including gap separator) with new + // anchor bytes. If the new anchor's shape needs a gap + // separator before existing relative content, insert one + // afterwards rather than copying the bytes through an + // intermediate Array. + let hasRelativeContent = relBegin < _storage.endIndex + let needsSep = hasRelativeContent + && _anchorNeedsGapSeparator(newAnchor._slice) + _storage.replaceSubrange( + _storage.startIndex..= 3 + } + +} + +// MARK: - Trailing separator + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// Whether this path ends with a directory separator that is + /// not structurally required by the path's anchor. + @available(SwiftStdlib 9999, *) + public var hasTrailingSeparator: Bool { + get { + guard !isEmpty else { return false } + if _storage._hasResourceForkSuffix() { return false } + let (rootEnd, relBegin) = _storage._parseRoot() + _internalInvariant(relBegin >= rootEnd) + if relBegin < _storage.endIndex { + // Has relative content; trailing sep is the last byte + return _isSeparator(_storage[_storage.index(before: _storage.endIndex)]) + } else if relBegin > rootEnd { + // No relative content, but a gap separator exists between + // the anchor and the end of the string (e.g. `\\server\share\` + // or `/.vol/1234/5678/`). That gap separator IS the trailing + // separator. + _internalInvariant(relBegin == _storage.endIndex) + _internalInvariant(_isSeparator(_storage[rootEnd])) + return true + } + // Anchor-only or empty root, no trailing separator + return false + } + set { + if newValue == hasTrailingSeparator { return } + if !newValue { + // Remove the trailing separator. The getter returned true, so the + // last byte is a separator; drop it unless it's the structural gap + // separator (when relBegin > rootEnd, e.g. `\\server\share\`), + // which belongs to the anchor. + let (_, relBegin) = _storage._parseRoot() + if _storage.index(before: _storage.endIndex) >= relBegin { + _storage.removeLast() + } + return + } + // Add a trailing separator. + if isEmpty { return } + if let rsrcStart = _storage._resourceForkSuffixStart { + _storage.removeSubrange(rsrcStart..<_storage.endIndex) + } + if !_isSeparator(_storage.last!) { + _storage.append(_platformSeparator) + } + } + } + + /// Returns a copy with a trailing separator added. + @available(SwiftStdlib 9999, *) + public func withTrailingSeparator() -> FilePath { + var copy = self + copy.hasTrailingSeparator = true + return copy + } + + /// Returns a copy with the trailing separator removed. + @available(SwiftStdlib 9999, *) + public func withoutTrailingSeparator() -> FilePath { + var copy = self + copy.hasTrailingSeparator = false + return copy + } +} + +// MARK: - Resource fork (Darwin) +// +// Implemented only on Darwin builds; the getter returns `false` on other +// platforms and the setter is a no-op (the helpers in FilePathDarwin.swift +// guard on `_isDarwin`, which folds to `false` at compile time elsewhere). + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// Whether this path ends with a resource fork reference. + @available(SwiftStdlib 9999, *) + public var isResourceFork: Bool { + get { _storage._hasResourceForkSuffix() } + set { + if newValue == isResourceFork { return } + if !newValue { + // Remove the resource fork suffix. + if let rsrcStart = _storage._resourceForkSuffixStart { + _storage.removeSubrange(rsrcStart..<_storage.endIndex) + } + return + } + // Add the resource fork suffix. The literal starts with `/`, so + // when storage already ends in a separator we drop the leading `/` + // to avoid storing two in a row. + if hasTrailingSeparator { + hasTrailingSeparator = false + } + let suffix = _SystemString._resourceForkSuffix._asciiBytes + if !_storage.isEmpty && _isSeparator(_storage.last!) { + _storage.append(contentsOf: suffix.dropFirst()) + } else { + _storage.append(contentsOf: suffix) + } + } + } + + /// Returns a copy with resource fork suffix appended. + @available(SwiftStdlib 9999, *) + public func withResourceFork() -> FilePath { + var copy = self + copy.isResourceFork = true + return copy + } + + /// Returns a copy with resource fork suffix removed. + @available(SwiftStdlib 9999, *) + public func withoutResourceFork() -> FilePath { + var copy = self + copy.isResourceFork = false + return copy + } +} + +// MARK: - Reconstruction initializers + +@available(SwiftStdlib 9999, *) +extension FilePath { + /// Creates a file path from a decomposed form. + @available(SwiftStdlib 9999, *) + public init( + anchor: Anchor?, + _ components: some Sequence, + hasTrailingSeparator: Bool = false + ) { + var str = _SystemString() + + if let anchor = anchor { + str.append(contentsOf: anchor._slice) + } + // Whether a separator is needed between the anchor and the first + // component, when one or more components follow. + let anchorNeedsSep = anchor.map { + _anchorNeedsGapSeparator($0._slice) + } ?? false + + var hasComponents = false + for comp in components { + // First component: separator iff the anchor's shape needs one. + // Subsequent components: always a separator. + if hasComponents || anchorNeedsSep { + str.append(_platformSeparator) + } + str.append(contentsOf: comp._slice) + hasComponents = true + } + + if hasTrailingSeparator { + if hasComponents { + str.append(_platformSeparator) + } else if let anchor = anchor, let last = anchor._slice.last, + !_isSeparator(last) { + // Trailing sep on an anchor-only path (e.g., `\\server\share\`): + // add a separator only if the anchor doesn't already end with one. + str.append(_platformSeparator) + } + } + + self.init(_normalizing: str) + } + + /// Creates a file path from a decomposed form with a resource fork suffix. + @available(SwiftStdlib 9999, *) + public init( + anchor: Anchor?, + _ components: some Sequence, + resourceFork: Bool + ) { + self.init(anchor: anchor, components, hasTrailingSeparator: false) + if resourceFork { + self.isResourceFork = true + } + } +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePathInternals.swift b/Sources/System/StdlibFilePathImplementation/FilePathInternals.swift new file mode 100644 index 00000000..cd6beceb --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathInternals.swift @@ -0,0 +1,85 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +// MARK: - Internal invariants + +#if FILEPATH_PACKAGE +@inline(__always) +internal func _internalInvariant( + _ condition: @autoclosure () -> Bool, + _ message: @autoclosure () -> String = "", + file: StaticString = #file, + line: UInt = #line +) { + assert(condition(), message(), file: file, line: line) +} +#endif + +// MARK: - Slice helpers + +extension Slice where Element: Equatable { + internal mutating func _eat(if p: (Element) -> Bool) -> Element? { + guard let s = self.first, p(s) else { return nil } + self = self.dropFirst() + return s + } + internal mutating func _eat(_ e: Element) -> Element? { + _eat(if: { $0 == e }) + } + + internal mutating func _eat(count c: Int) -> Slice { + defer { self = self.dropFirst(c) } + return self.prefix(c) + } + + internal mutating func _eatSequence( + _ es: C + ) -> Slice? where C.Element == Element { + guard self.starts(with: es) else { return nil } + return _eat(count: es.count) + } + + internal mutating func _eatUntil(_ idx: Index) -> Slice { + precondition(idx >= startIndex && idx <= endIndex) + defer { self = self[idx...] } + return self[.. Bool + ) -> Slice? { + let idx = firstIndex(where: { !p($0) }) ?? endIndex + guard idx != startIndex else { return nil } + return _eatUntil(idx) + } +} + +extension MutableCollection where Element: Equatable { + mutating func _replaceAll(_ e: Element, with new: Element) { + for idx in self.indices { + if self[idx] == e { self[idx] = new } + } + } +} + +// MARK: - ASCII byte conversion + +@available(SwiftStdlib 9999, *) +extension String { + /// A view over the string's bytes as `FilePath.CodeUnit`s, lazily mapped. + /// + /// Intended for ASCII string literals used as match/replace tokens — no + /// allocation, materialized only when iterated. Non-ASCII scalars trap. + internal var _asciiBytes: LazyMapCollection< + String.UnicodeScalarView, FilePath.CodeUnit + > { + self.unicodeScalars.lazy.map { FilePath.CodeUnit(_ascii: $0) } + } +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePathParsing.swift b/Sources/System/StdlibFilePathImplementation/FilePathParsing.swift new file mode 100644 index 00000000..5ae3db7b --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathParsing.swift @@ -0,0 +1,259 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +// MARK: - Platform predicates +// +// Compile-time platform selection. This reference implementation builds for a +// single platform at a time, so these fold to constants. +#if os(Windows) +internal var _isWindows: Bool { true } +internal var _isDarwin: Bool { false } +internal var _isLinux: Bool { false } +#elseif os(anyAppleOS) || canImport(Darwin) +internal var _isWindows: Bool { false } +internal var _isDarwin: Bool { true } +internal var _isLinux: Bool { false } +#elseif os(Linux) +internal var _isWindows: Bool { false } +internal var _isDarwin: Bool { false } +internal var _isLinux: Bool { true } +#else +#error("FilePath: unsupported platform") +#endif + +// The separator we use for slash-based platforms +@available(SwiftStdlib 9999, *) +private var _genericSeparator: FilePath.CodeUnit { ._slash } + +@available(SwiftStdlib 9999, *) +internal var _platformSeparator: FilePath.CodeUnit { + _isWindows ? ._backslash : _genericSeparator +} + +@available(SwiftStdlib 9999, *) +internal func _isSeparator(_ c: FilePath.CodeUnit) -> Bool { + c == _platformSeparator +} + +// MARK: - Anchor shape classification + +/// Returns `true` if the given anchor bytes are the Windows +/// drive-relative form `:` (e.g. `C:`). +/// +/// **Precondition: caller is on Windows.** Drive-relative is a +/// Windows-specific concept; this function asserts `_isWindows` and +/// must not be called from cross-platform code without a `_isWindows` +/// gate. (See `_anchorNeedsGapSeparator` for the canonical example.) +/// +/// The `:` IS the boundary in this anchor: `C:foo` is valid +/// (drive-relative with one component); `C:\foo` is a different +/// anchor (drive-absolute). Drive-relative is the *only* 2-byte +/// anchor on Windows: UNC (`\\server\share`) and verbatim variants +/// are all longer; other anchor shapes that happen to end in `:` — +/// UNC with a colon-ending share name (`\\server\C:`), or volfs-style +/// FILEIDs on hypothetical cross-platform code — are NOT 2 bytes +/// and don't match. +@available(SwiftStdlib 9999, *) +internal func _isDriveRelativeAnchor( + _ anchorBytes: some BidirectionalCollection +) -> Bool { + _internalInvariant(_isWindows, "drive-relative anchor is Windows-specific") + return anchorBytes.count == 2 && anchorBytes.last == ._colon +} + +/// Returns `true` if a separator must be inserted between the given +/// anchor bytes and the first component byte that follows them. +/// +/// No separator is needed when: +/// - the anchor's last byte is already a separator (most cases: +/// `/`, `C:\`, `\\?\C:\`, `\\server\share\`, `/.nofollow/`, etc.), or +/// - the anchor is the Windows drive-relative form `:`, where +/// the `:` itself IS the boundary (`C:foo` is valid; `C:\foo` is a +/// different anchor — drive-absolute). +/// +/// A separator IS needed for the other shapes whose last byte is a +/// name byte: `\\server\share`, `\\?\UNC\server\share`, `\\?\name`, +/// `/.vol/FSID/FILEID` — including degenerate cases where one of +/// those name bytes happens to be `:` (e.g. UNC share `\\server\C:` +/// or volfs FILEID ending in `:`). Those are NOT 2 bytes, so they +/// don't match the drive-relative shape. +@available(SwiftStdlib 9999, *) +internal func _anchorNeedsGapSeparator( + _ anchorBytes: some BidirectionalCollection +) -> Bool { + guard let last = anchorBytes.last else { return false } + if _isSeparator(last) { return false } + if _isWindows && _isDriveRelativeAnchor(anchorBytes) { return false } + return true +} + +// MARK: - Root parsing + +@available(SwiftStdlib 9999, *) +extension _SystemString { + internal func _parseRoot() -> ( + rootEnd: Index, relativeBegin: Index + ) { + let result: (rootEnd: Index, relativeBegin: Index) + + if isEmpty { + result = (startIndex, startIndex) + } else if _isWindows { + result = _parseWindowsRoot() + } else if !_isSeparator(self.first!) { + result = (startIndex, startIndex) + } else if _isDarwin, let darwinAnchor = _parseDarwinAnchor() { + result = (darwinAnchor.anchorEnd, darwinAnchor.relativeBegin) + } else { + let next = self.index(after: startIndex) + result = (next, next) + } + + _internalInvariant(result.rootEnd >= startIndex && result.rootEnd <= endIndex) + _internalInvariant(result.relativeBegin >= result.rootEnd) + _internalInvariant(result.relativeBegin <= endIndex) + // Gap between rootEnd and relativeBegin is at most one separator + _internalInvariant(distance(from: result.rootEnd, to: result.relativeBegin) <= 1) + return result + } +} + +// MARK: - Separator normalization + +@available(SwiftStdlib 9999, *) +extension _SystemString { + // Coalesce repeated separators in place. On Windows, also convert `/` + // to `\` (verbatim-aware) and prenormalize roots before coalescing. + // Trailing separators are preserved. + internal mutating func _normalizeSeparators() { + guard !isEmpty else { return } + var (writeIdx, readIdx) = (startIndex, startIndex) + + if _isWindows { + // Detect exact \\?\ prefix on raw input before any conversion. + // Only exact backslashes trigger verbatim mode. Inside a verbatim + // anchor `/` is a legal component byte and we leave it alone; the + // anchor region itself is already all-backslash by definition. + if _findVerbatimAnchorEnd() == startIndex { + self._replaceAll(_genericSeparator, with: _platformSeparator) + // //?/ normalizes to \\?\ after conversion, but it's + // device-namespace, not verbatim. Demote ? → . sigil. + if _startsWithVerbatimPrefix() != nil { + self[index(startIndex, offsetBy: 2)] = ._dot + } + } + readIdx = _prenormalizeWindowsRoots() + writeIdx = readIdx + + while readIdx < endIndex && _isSeparator(self[readIdx]) { + self.formIndex(after: &readIdx) + } + } + + while readIdx < endIndex { + _internalInvariant(writeIdx <= readIdx) + + let wasSeparator = _isSeparator(self[readIdx]) + self.swapAt(writeIdx, readIdx) + self.formIndex(after: &writeIdx) + self.formIndex(after: &readIdx) + + while wasSeparator, readIdx < endIndex, _isSeparator(self[readIdx]) { + self.formIndex(after: &readIdx) + } + } + _internalInvariant(readIdx == endIndex) + self.removeLast(self.distance(from: writeIdx, to: readIdx)) + } +} + +// MARK: - Dot normalization (new rules for SE-0529) + +@available(SwiftStdlib 9999, *) +extension _SystemString { + // Append the dot-normalized form of `self[range]` to `result`. Rules: + // - `.` is dropped unless it is the leading component of an unrooted path + // - Trailing `.` becomes a trailing separator (foo/. -> foo/) + // - `..` is always preserved + // + // `range` is the relative portion to normalize — anchor and gap bytes, + // if any, must already be in `result`. Verbatim Windows paths (where + // `.` and `..` are regular component names) skip this entirely; the + // caller copies bytes verbatim instead. + // + // `isRooted` controls leading-dot behavior: a leading `.` is dropped + // when the path is rooted, kept when not. + // + // Returns `true` iff at least one component byte was appended. Callers + // use this to roll back a speculatively-inserted anchor/relative + // separator when the relative portion dot-normalizes to empty. + internal func _normalizeDots( + over range: Range, + isRooted: Bool, + into result: inout _SystemString + ) -> Bool { + // Precondition: the caller has already placed any anchor + gap bytes + // into `result`, so the range covers the relative portion only — never + // starts on a separator. (For Windows UNC, this is the difference + // between `rootEnd` and `relativeBegin`.) + _internalInvariant( + range.lowerBound >= startIndex && range.upperBound <= endIndex) + _internalInvariant( + range.isEmpty || !_isSeparator(self[range.lowerBound]), + "_normalizeDots range must start past any gap separator") + + var readIdx = range.lowerBound + let end = range.upperBound + var componentIndex = 0 + var emittedAny = false + var lastDroppedADot = false + var sourceHadTrailingSep = false + + while readIdx < end { + // Skip a separator. If it is the last byte of the range, remember + // that the source had a trailing separator. + if _isSeparator(self[readIdx]) { + let next = index(after: readIdx) + if next >= end { + sourceHadTrailingSep = true + } + readIdx = next + continue + } + // Read one component span. + let compStart = readIdx + while readIdx < end && !_isSeparator(self[readIdx]) { + readIdx = index(after: readIdx) + } + let compEnd = readIdx + let compLen = distance(from: compStart, to: compEnd) + let isDot = compLen == 1 && self[compStart] == ._dot + + // Drop a `.` unless it is the leading component of an unrooted path. + let drop = isDot && !(componentIndex == 0 && !isRooted) + if drop { + lastDroppedADot = true + } else { + if emittedAny { + result.append(_platformSeparator) + } + result.append(contentsOf: self[compStart.. FilePath { +#if FILEPATH_PACKAGE +#if os(Windows) + return try _resolveWindows() +#elseif canImport(Darwin) + return try _resolveDarwin() +#else + return try _resolveLinux() +#endif +#else + // Stdlib port: dispatch to the runtime stub, which is the only place in + // the stdlib build that imports platform headers. See + // `swift/stdlib/public/SwiftShims/swift/shims/FilePath.h` and + // `swift/stdlib/public/stubs/FilePathStubs.cpp`. + return try _resolveViaStdlibStub() +#endif + } +} + +#if !FILEPATH_PACKAGE + +@available(SwiftStdlib 9999, *) +extension FilePath { + fileprivate func _resolveViaStdlibStub() throws -> FilePath { + var outBuf: UnsafeMutablePointer? = nil + var outCount: __swift_size_t = 0 + let err: CInt = unsafe self.withCodeUnits { ptr, count in + unsafe _swift_stdlib_FilePath_resolve( + ptr, __swift_size_t(count), &outBuf, &outCount) + } + guard err == 0, let resultBuf = unsafe outBuf else { + throw _FilePathResolveError(code: err) + } + defer { unsafe _swift_stdlib_free(resultBuf) } + return unsafe FilePath( + _normalizingRawCodeUnits: UnsafeRawPointer(resultBuf), + count: Int(outCount)) + } +} + +#endif // !FILEPATH_PACKAGE + +// Build a FilePath from `count` platform code units starting at `ptr`. +// The bytes must already be a valid path (no embedded NUL); the result +// is fed through `_normalizing`, which canonicalizes whatever +// per-platform anchor / suffix the bytes happen to express. One +// allocation (the `_SystemString` storage) plus one bulk copy — the +// backing `Array` is sized exactly and filled in place rather than via +// per-element `append`. +@available(SwiftStdlib 9999, *) +extension FilePath { + fileprivate init( + _normalizingRawCodeUnits ptr: UnsafeRawPointer, + count: Int + ) { + _internalInvariant(count >= 0) + let chars = unsafe Array( + unsafeUninitializedCapacity: count + 1 + ) { buf, initializedCount in + if count > 0 { + let byteCount = count * MemoryLayout.stride + unsafe UnsafeMutableRawPointer(buf.baseAddress!) + .copyMemory(from: ptr, byteCount: byteCount) + } + unsafe buf[count] = ._null + initializedCount = count + 1 + } + self.init(_normalizing: _SystemString(nullTerminated: chars)) + } +} + +#if FILEPATH_PACKAGE + +// MARK: - Per-platform implementation + +#if os(Windows) + +// MARK: Windows + +@available(SwiftStdlib 9999, *) +extension FilePath { + fileprivate func _resolveWindows() throws -> FilePath { + let shareMode: DWORD = + DWORD(FILE_SHARE_READ) | DWORD(FILE_SHARE_WRITE) | DWORD(FILE_SHARE_DELETE) + + let handle: HANDLE = self._storage.withNullTerminatedCodeUnits { p in + unsafe CreateFileW( + p.baseAddress, + 0, + shareMode, + nil, + DWORD(OPEN_EXISTING), + DWORD(FILE_FLAG_BACKUP_SEMANTICS), + nil) + } + if handle == INVALID_HANDLE_VALUE { + throw _FilePathResolveError(code: CInt(GetLastError())) + } + defer { unsafe _ = CloseHandle(handle) } + + let flags: DWORD = DWORD(FILE_NAME_NORMALIZED) | DWORD(VOLUME_NAME_DOS) + var capacity: Int = 1024 + while true { + var buf = [WCHAR](repeating: 0, count: capacity) + let needed: DWORD = unsafe buf.withUnsafeMutableBufferPointer { ptr in + unsafe GetFinalPathNameByHandleW( + handle, ptr.baseAddress, DWORD(ptr.count), flags) + } + if needed == 0 { + throw _FilePathResolveError(code: CInt(GetLastError())) + } + // On success `needed` is the length WITHOUT the NUL (so it fits with + // room to spare). On buffer-too-small it's the required size INCLUDING + // the NUL — grow and retry. + if Int(needed) < capacity { + return unsafe buf.withUnsafeBufferPointer { ptr in + unsafe FilePath( + _normalizingRawCodeUnits: UnsafeRawPointer(ptr.baseAddress!), + count: Int(needed)) + } + } + capacity = Int(needed) + 1 + } + } +} + +#elseif canImport(Darwin) + +// MARK: Darwin +// +// realpath(3) returns the un-firmlinked underlay (e.g. +// `/System/Volumes/Data/Users/foo`) for paths whose components live on the +// data volume but are exposed via firmlinks (the typical case for `/Users` +// on macOS Big Sur+). The kernel's *default* behavior for +// `ATTR_CMN_FULLPATH` returns the firmlinked path instead — pinned for +// this build by `ResolveTests.firmlinkedHomeRealPath`. So we use +// `getattrlistat` directly with `ATTR_CMN_FULLPATH` and avoid `realpath` +// entirely. + +@available(SwiftStdlib 9999, *) +extension FilePath { + fileprivate func _resolveDarwin() throws -> FilePath { + // Two attempts: 8 KiB suits any normal path, 32 KiB covers + // long-path-enabled processes. Past that, the path is genuinely + // too long. + for size in [8192, 32768] { + if let resolved = try _resolveDarwinAttempt(bufferSize: size) { + return resolved + } + } + throw _FilePathResolveError(code: ENAMETOOLONG) + } + + // Returns nil ONLY when the buffer was too small and the caller should + // retry with a larger one (ERANGE / ENAMETOOLONG). Other errors throw. + private func _resolveDarwinAttempt( + bufferSize: Int + ) throws -> FilePath? { + var attrs = attrlist() + attrs.bitmapcount = UInt16(ATTR_BIT_MAP_COUNT) + attrs.commonattr = attrgroup_t(ATTR_CMN_FULLPATH) + + let options: CUnsignedLong = + CUnsignedLong(FSOPT_ATTR_CMN_EXTENDED) | + CUnsignedLong(FSOPT_RETURN_REALDEV) + + let bufRaw = UnsafeMutableRawPointer.allocate( + byteCount: bufferSize, + alignment: MemoryLayout.alignment) + defer { unsafe bufRaw.deallocate() } + + let rc: CInt = unsafe self._storage.withNullTerminatedCodeUnits { pathBuf in + unsafe withUnsafeMutablePointer(to: &attrs) { attrsPtr in + unsafe getattrlistat( + AT_FDCWD, + pathBuf.baseAddress, + attrsPtr, + bufRaw, + bufferSize, + options) + } + } + if rc != 0 { + let err = errno + if err == ERANGE || err == ENAMETOOLONG { + return nil + } + throw _FilePathResolveError(code: err) + } + + // Buffer layout when ATTR_CMN_FULLPATH is the only attribute requested: + // buf[0..4] u_int32_t total bytes used (we ignore this header) + // buf[4..12] attrreference_t for the FULLPATH attribute + // .attr_dataoffset offset relative to the start of + // the attrreference_t struct itself + // .attr_length bytes (includes trailing NUL) + // buf[4 + .attr_dataoffset ..] the resolved C-string path + let attrrefOffset = MemoryLayout.size + let attrref: attrreference_t = unsafe bufRaw + .advanced(by: attrrefOffset) + .load(as: attrreference_t.self) + let stringStart = unsafe bufRaw + .advanced(by: attrrefOffset + Int(attrref.attr_dataoffset)) + let storedLength = Int(attrref.attr_length) + + // ATTR_CMN_FULLPATH is documented as a null-terminated string and + // attr_length includes the NUL. Both are kernel guarantees; assert them + // rather than handling a "no trailing NUL" branch that can't fire. + _internalInvariant(storedLength > 0) + _internalInvariant( + unsafe stringStart.load( + fromByteOffset: storedLength - 1, as: UInt8.self) == 0, + "ATTR_CMN_FULLPATH must be null-terminated") + + return unsafe FilePath( + _normalizingRawCodeUnits: stringStart, count: storedLength - 1) + } +} + +#else + +// MARK: Linux (and other POSIX) +// +// Linux has neither firmlinks nor Darwin-style anchor prefixes, so the +// portable POSIX call is correct. + +@available(SwiftStdlib 9999, *) +extension FilePath { + fileprivate func _resolveLinux() throws -> FilePath { + let resolved = self._storage.withNullTerminatedCodeUnits { p in + unsafe realpath(p.baseAddress, nil) + } + guard let resolved = resolved else { + throw _FilePathResolveError(code: errno) + } + defer { unsafe free(resolved) } + + let length = unsafe strlen(resolved) + return unsafe FilePath(_normalizingRawCodeUnits: resolved, count: length) + } +} + +#endif + +#endif // FILEPATH_PACKAGE diff --git a/Sources/System/StdlibFilePathImplementation/FilePathStringBridging.swift b/Sources/System/StdlibFilePathImplementation/FilePathStringBridging.swift new file mode 100644 index 00000000..43e55a21 --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathStringBridging.swift @@ -0,0 +1,123 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +// MARK: - FilePath String bridging + +@available(SwiftStdlib 9999, *) +extension FilePath: Hashable { + @available(SwiftStdlib 9999, *) + public static func == (lhs: FilePath, rhs: FilePath) -> Bool { + lhs._storage == rhs._storage + } + + @available(SwiftStdlib 9999, *) + public func hash(into hasher: inout Hasher) { + hasher.combine(_storage) + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath: Comparable { + @available(SwiftStdlib 9999, *) + public static func < (lhs: FilePath, rhs: FilePath) -> Bool { + lhs._storage.lexicographicallyPrecedes(rhs._storage) + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath: CustomStringConvertible, CustomDebugStringConvertible { + /// A textual representation of the file path. + @available(SwiftStdlib 9999, *) + public var description: String { + unsafe _storage.withCodeUnits { codeUnits in + unsafe codeUnits.withMemoryRebound(to: FilePath._Encoding.CodeUnit.self) { + unsafe String(decoding: $0, as: FilePath._Encoding.self) + } + } + } + + @available(SwiftStdlib 9999, *) + public var debugDescription: String { + description.debugDescription + } +} + +@available(SwiftStdlib 9999, *) +extension FilePath: ExpressibleByStringLiteral { + /// Creates a file path from a string literal. + /// + /// Traps if the literal contains `NUL` or is otherwise ill-formed. + @available(SwiftStdlib 9999, *) + public init(stringLiteral: String) { + guard let path = FilePath(stringLiteral) else { + fatalError( + "FilePath string literal must not contain NUL") + } + self = path + } + + /// Creates a file path from a string. + /// + /// Returns `nil` if `string` contains `NUL`, which is not a valid + /// path byte on any supported platform. + @available(SwiftStdlib 9999, *) + public init?(_ string: String) { + guard !string.utf8.contains(0) else { return nil } + self.init(_normalizing: _SystemString(string)) + } +} + +// MARK: - String decoding/validating + +extension String { + @available(SwiftStdlib 9999, *) + public init(decoding path: FilePath) { + self = path.description + } + + @available(SwiftStdlib 9999, *) + public init?(validating path: FilePath) { + guard let str = String(validating: path._storage) else { return nil } + self = str + } + + @available(SwiftStdlib 9999, *) + public init(decoding anchor: FilePath.Anchor) { + self = anchor.description + } + + @available(SwiftStdlib 9999, *) + public init?(validating anchor: FilePath.Anchor) { + // Mirror the FilePath overload: decode, re-encode, and compare (via + // String(validating: _SystemString)), so ill-formed content yields nil + // instead of a lossy U+FFFD decode. + guard let str = String(validating: _SystemString(anchor._slice)) else { + return nil + } + self = str + } + + @available(SwiftStdlib 9999, *) + public init(decoding component: FilePath.Component) { + self = component.description + } + + @available(SwiftStdlib 9999, *) + public init?(validating component: FilePath.Component) { + // Mirror the FilePath overload: decode, re-encode, and compare (via + // String(validating: _SystemString)), so ill-formed content yields nil + // instead of a lossy U+FFFD decode. + guard let str = String(validating: _SystemString(component._slice)) else { + return nil + } + self = str + } +} + diff --git a/Sources/System/StdlibFilePathImplementation/FilePathSystemString.swift b/Sources/System/StdlibFilePathImplementation/FilePathSystemString.swift new file mode 100644 index 00000000..d8b1919d --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathSystemString.swift @@ -0,0 +1,240 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +// MARK: - FilePath.CodeUnit helpers +// +// `FilePath.CodeUnit` is the storage element type used throughout this +// implementation: `CChar` on Unix, `UInt16` on Windows. The underscored +// extension members below are FilePath-internal helpers — they extend +// the underlying `CChar` / `UInt16` type with names a reader can +// recognise as path-byte constants. + +@available(SwiftStdlib 9999, *) +extension FilePath.CodeUnit { + internal static var _null: Self { 0 } + internal static var _slash: Self { Self(_ascii: "/") } + internal static var _backslash: Self { Self(_ascii: #"\"#) } + internal static var _dot: Self { Self(_ascii: ".") } + internal static var _colon: Self { Self(_ascii: ":") } + internal static var _question: Self { Self(_ascii: "?") } + internal static var _at: Self { Self(_ascii: "@") } + + internal init(_ascii s: Unicode.Scalar) { + self = numericCast(UInt8(ascii: s)) + } + + /// Interpret this code unit as a drive-letter scalar, presented as + /// written (no case normalization). On Windows, code units are + /// UTF-16 and an unpaired surrogate yields `U+FFFD`. + internal var _driveLetterScalar: Unicode.Scalar { + #if os(Windows) + return Unicode.Scalar(self) ?? "\u{FFFD}" + #else + return Unicode.Scalar(UInt8(bitPattern: self)) + #endif + } +} + +@available(SwiftStdlib 9999, *) +internal struct _SystemString: Sendable { + internal typealias _Storage = [FilePath.CodeUnit] + internal var nullTerminatedStorage: _Storage +} + +@available(SwiftStdlib 9999, *) +extension _SystemString { + internal init() { + self.nullTerminatedStorage = [._null] + _invariantCheck() + } + + internal var length: Int { + let len = nullTerminatedStorage.count - 1 + _internalInvariant(len == self.count) + return len + } + + internal init(nullTerminated storage: _Storage) { + self.nullTerminatedStorage = storage + _invariantCheck() + } +} + +@available(SwiftStdlib 9999, *) +extension _SystemString { + fileprivate func _invariantsSatisfied() -> Bool { + guard !nullTerminatedStorage.isEmpty else { return false } + guard nullTerminatedStorage.last! == ._null else { return false } + guard nullTerminatedStorage.firstIndex(of: ._null) == nullTerminatedStorage.count - 1 else { + return false + } + return true + } + + fileprivate func _invariantCheck() { + #if DEBUG + precondition(_invariantsSatisfied()) + #endif + } +} + +@available(SwiftStdlib 9999, *) +extension _SystemString: RandomAccessCollection, MutableCollection { + internal typealias Element = FilePath.CodeUnit + internal typealias Index = _Storage.Index + internal typealias Indices = Range + + internal var startIndex: Index { + nullTerminatedStorage.startIndex + } + + internal var endIndex: Index { + nullTerminatedStorage.index(before: nullTerminatedStorage.endIndex) + } + + internal subscript(position: Index) -> FilePath.CodeUnit { + _read { + precondition(position >= startIndex && position <= endIndex) + yield nullTerminatedStorage[position] + } + set(newValue) { + precondition(position >= startIndex && position <= endIndex) + nullTerminatedStorage[position] = newValue + _invariantCheck() + } + } +} +@available(SwiftStdlib 9999, *) +extension _SystemString: RangeReplaceableCollection { + internal mutating func replaceSubrange( + _ subrange: Range, with newElements: C + ) where C.Element == FilePath.CodeUnit { + defer { _invariantCheck() } + nullTerminatedStorage.replaceSubrange(subrange, with: newElements) + } + + internal mutating func reserveCapacity(_ n: Int) { + defer { _invariantCheck() } + nullTerminatedStorage.reserveCapacity(1 + n) + } + + internal func withContiguousStorageIfAvailable( + _ body: (UnsafeBufferPointer) throws -> R + ) rethrows -> R? { + try unsafe nullTerminatedStorage.withContiguousStorageIfAvailable { + try unsafe body(.init(start: $0.baseAddress, count: $0.count-1)) + } + } + + internal mutating func withContiguousMutableStorageIfAvailable( + _ body: (inout UnsafeMutableBufferPointer) throws -> R + ) rethrows -> R? { + defer { _invariantCheck() } + return try unsafe nullTerminatedStorage.withContiguousMutableStorageIfAvailable { + var buffer = unsafe UnsafeMutableBufferPointer( + start: $0.baseAddress, count: $0.count-1 + ) + return try unsafe body(&buffer) + } + } +} + +@available(SwiftStdlib 9999, *) +extension _SystemString: Hashable {} + +@available(SwiftStdlib 9999, *) +extension _SystemString { + // _Storage backing — includes the trailing null byte. + internal func withNullTerminatedCodeUnits( + _ f: (UnsafeBufferPointer) throws -> T + ) rethrows -> T { + try unsafe nullTerminatedStorage.withUnsafeBufferPointer(f) + } + + // Code units excluding the null terminator. + internal func withCodeUnits( + _ f: (UnsafeBufferPointer) throws -> T + ) rethrows -> T { + try unsafe withNullTerminatedCodeUnits { + unsafe _internalInvariant($0.last == ._null) + return try unsafe f(.init(start: $0.baseAddress, count: $0.count &- 1)) + } + } +} + +// MARK: - Span access + +@available(SwiftStdlib 9999, *) +extension _SystemString { + // The backing array INCLUDING the trailing null terminator. Per-subtype + // spans extract their slice range from this base. + internal var _nullTerminatedSpan: Span { + nullTerminatedStorage.span + } + + // The backing array EXCLUDING the trailing null terminator. + internal var _span: Span { + nullTerminatedStorage.span.extracting(0.. { + internal func withCodeUnits( + _ f: (UnsafeBufferPointer) throws -> T + ) rethrows -> T { + try unsafe base.nullTerminatedStorage.withUnsafeBufferPointer { fullBuf in + let count = self.count + _internalInvariant(startIndex >= 0 && startIndex + count <= fullBuf.count) + let p = unsafe fullBuf.baseAddress.map { unsafe $0.advanced(by: startIndex) } + return try unsafe f(UnsafeBufferPointer(start: p, count: count)) + } + } +} + +@available(SwiftStdlib 9999, *) +extension String { + internal init?(validating str: _SystemString) { + let decoded = str.string + guard _SystemString(decoded) == str else { return nil } + self = decoded + } +} + +@available(SwiftStdlib 9999, *) +extension _SystemString: ExpressibleByStringLiteral { + internal init(stringLiteral: String) { + self.init(stringLiteral) + } + + internal init(_ string: String) { + #if os(Windows) + var chars = string.utf16.map { FilePath.CodeUnit($0) } + #else + var chars = string.utf8.map { FilePath.CodeUnit(bitPattern: $0) } + #endif + chars.append(._null) + self.init(nullTerminated: chars) + } +} + +@available(SwiftStdlib 9999, *) +extension _SystemString: CustomStringConvertible, CustomDebugStringConvertible { + internal var string: String { + unsafe self.withCodeUnits { codeUnits in + unsafe codeUnits.withMemoryRebound(to: FilePath._Encoding.CodeUnit.self) { + unsafe String(decoding: $0, as: FilePath._Encoding.self) + } + } + } + + internal var description: String { string } + internal var debugDescription: String { description.debugDescription } +} diff --git a/Sources/System/StdlibFilePathImplementation/FilePathWindows.swift b/Sources/System/StdlibFilePathImplementation/FilePathWindows.swift new file mode 100644 index 00000000..8b3ef49e --- /dev/null +++ b/Sources/System/StdlibFilePathImplementation/FilePathWindows.swift @@ -0,0 +1,391 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2020 - 2026 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +*/ + +// MARK: - Parsed Windows root + +@available(SwiftStdlib 9999, *) +internal struct _ParsedWindowsRoot { + var rootEnd: _SystemString.Index + var relativeBegin: _SystemString.Index + var drive: FilePath.CodeUnit? + var deviceSigil: FilePath.CodeUnit? +} + +@available(SwiftStdlib 9999, *) +extension _ParsedWindowsRoot { + static func traditional( + drive: FilePath.CodeUnit?, + endingAt idx: _SystemString.Index + ) -> _ParsedWindowsRoot { + _ParsedWindowsRoot( + rootEnd: idx, + relativeBegin: idx, + drive: drive, + deviceSigil: nil) + } + + static func unc( + deviceSigil: FilePath.CodeUnit?, + endingAt end: _SystemString.Index, + relativeBegin relBegin: _SystemString.Index + ) -> _ParsedWindowsRoot { + _ParsedWindowsRoot( + rootEnd: end, + relativeBegin: relBegin, + drive: nil, + deviceSigil: deviceSigil) + } + + static func device( + deviceSigil: FilePath.CodeUnit, + drive: FilePath.CodeUnit?, + endingAt end: _SystemString.Index, + relativeBegin relBegin: _SystemString.Index + ) -> _ParsedWindowsRoot { + _ParsedWindowsRoot( + rootEnd: end, + relativeBegin: relBegin, + drive: drive, + deviceSigil: deviceSigil) + } + + var isVerbatimComponent: Bool { + deviceSigil == ._question + } +} + +// MARK: - Lexer + +@available(SwiftStdlib 9999, *) +struct _Lexer { + var slice: Slice<_SystemString> + + init(_ str: _SystemString) { + self.slice = str[...] + } + + var backslash: FilePath.CodeUnit { ._backslash } + + mutating func eatBackslash() -> Bool { + slice._eat(._backslash) != nil + } + + // A drive letter is any single non-separator code unit immediately + // followed by a colon. This matches Windows' own + // RtlDetermineDosPathNameType_U, which keys on the second character + // being a colon and does not validate the first: `1:`, `::`, etc. are + // all drives. (A colon is itself a non-separator, so `::foo` parses as + // drive `:` — the documented basis for the `=::` environment variable + // Windows creates.) + // + // The non-separator requirement is what keeps leading-separator paths + // out: `\:x` (and `/:x`, which `_normalizeSeparators` has already + // rewritten to `\:x` by this point) are classified as rooted/UNC, not + // drives. Because `/`→`\` conversion has already run, `isSeparator` + // — which tests `\` only — is exactly the right predicate here. + mutating func eatDrive() -> FilePath.CodeUnit? { + let copy = slice + if let d = slice._eat(if: { !_isSeparator($0) }), + slice._eat(._colon) != nil { + return d + } + slice = copy + return nil + } + + mutating func eatSigil() -> FilePath.CodeUnit? { + let copy = slice + guard let sigil = slice._eat(._question) ?? slice._eat(._dot) else { + return nil + } + guard isEmpty || slice.first == backslash else { + slice = copy + return nil + } + return sigil + } + + mutating func eatUNC() -> Bool { + slice._eatSequence("UNC"._asciiBytes) != nil + } + + mutating func eatComponent() -> Range<_SystemString.Index> { + let backslash = self.backslash + let component = slice._eatWhile({ $0 != backslash }) + ?? slice[slice.startIndex ..< slice.startIndex] + return component.indices + } + + var isEmpty: Bool { + return slice.isEmpty + } + + var current: _SystemString.Index { slice.startIndex } + + mutating func clear() { + self = _Lexer(_SystemString()) + } + + mutating func reset(to str: _SystemString, at idx: _SystemString.Index) { + self.slice = str[idx...] + } +} + +// MARK: - Verbatim prefix detection (pre-normalization) + +@available(SwiftStdlib 9999, *) +extension _SystemString { + // Check if this string starts with the exact verbatim prefix \\?\ + // (four backslashes — no forward slashes). Returns the index past + // the prefix, or nil. + internal func _startsWithVerbatimPrefix() -> Index? { + var s = self[...] + guard s._eatSequence(#"\\?\"#._asciiBytes) != nil else { return nil } + return s.startIndex + } + + // For a verbatim path (exact \\?\ prefix), find where the anchor + // ends. Only backslash is a separator in verbatim context. + // Returns the index where component content begins. + internal func _findVerbatimAnchorEnd() -> Index { + guard let afterPrefix = _startsWithVerbatimPrefix() else { + return startIndex + } + + func skipToSep(from start: Index) -> Index { + var i = start + while i < endIndex && !_isSeparator(self[i]) { + formIndex(after: &i) + } + return i + } + + func skipPastSep(from idx: Index) -> Index { + idx < endIndex && _isSeparator(self[idx]) ? index(after: idx) : idx + } + + // \\?\UNC\server\share[\] + var s = self[afterPrefix...] + if s._eatSequence("UNC"._asciiBytes) != nil, + s._eat(._backslash) != nil { + let serverEnd = skipToSep(from: s.startIndex) + let shareStart = skipPastSep(from: serverEnd) + let shareEnd = skipToSep(from: shareStart) + return skipPastSep(from: shareEnd) + } + + // \\?\:[\] — a drive letter is any single non-separator code + // unit before the colon (matching eatDrive). In verbatim paths + // separators are not normalized and `/` is a legal component byte, + // so `!isSeparator` accepts it: `\\?\/:` parses with drive `/`, + // taking the bytes as written. + s = self[afterPrefix...] + if s._eat(if: { !_isSeparator($0) }) != nil, + s._eat(._colon) != nil { + return skipPastSep(from: s.startIndex) + } + + // \\?\device[\] + let deviceEnd = skipToSep(from: afterPrefix) + return skipPastSep(from: deviceEnd) + } +} + +// MARK: - Windows root parsing + +@available(SwiftStdlib 9999, *) +extension _SystemString { + internal func _parseWindowsRootInternal() -> _ParsedWindowsRoot? { + _internalInvariant(_isWindows) + + var lexer = _Lexer(self) + + func parseUNC( + deviceSigil: FilePath.CodeUnit? + ) -> _ParsedWindowsRoot { + _ = lexer.eatComponent() + guard lexer.eatBackslash() else { + let end = lexer.current + return .unc( + deviceSigil: deviceSigil, + endingAt: end, + relativeBegin: end) + } + _ = lexer.eatComponent() + let rootEnd = lexer.current + _ = lexer.eatBackslash() + return .unc( + deviceSigil: deviceSigil, + endingAt: rootEnd, relativeBegin: lexer.current) + } + + // `C:` or `C:\` + if let d = lexer.eatDrive() { + _ = lexer.eatBackslash() + return .traditional( + drive: d, + endingAt: lexer.current) + } + + guard lexer.eatBackslash() else { return nil } + guard lexer.eatBackslash() else { + return .traditional( + drive: nil, + endingAt: lexer.current) + } + + guard let sigil = lexer.eatSigil() else { + return parseUNC(deviceSigil: nil) + } + + guard lexer.eatBackslash() else { + return .device( + deviceSigil: sigil, + drive: nil, + endingAt: lexer.current, + relativeBegin: lexer.current) + } + + // UNC sub-form only applies to verbatim paths (\\?\UNC\...). + // For device-namespace (\\.\), UNC is just a device name. + if sigil == ._question, lexer.eatUNC() { + guard lexer.eatBackslash() else { + let end = lexer.current + return .device( + deviceSigil: sigil, + drive: nil, + endingAt: end, + relativeBegin: end) + } + return parseUNC(deviceSigil: sigil) + } + + // Check for device drive: \\.\C:\ or \\?\C:\ + let deviceRange = lexer.eatComponent() + let rootEnd = lexer.current + + // A drive letter is any single non-separator code unit before the + // colon (matching `eatDrive`). In verbatim paths `/` is a non-separator + // and legal, so `\\?\/:` parses with drive `/`. + let drive = _driveLetter(of: self[deviceRange]) + if drive != nil, lexer.eatBackslash() { + // \\?\C:\ or \\.\C:\ + let newEnd = lexer.current + return .device( + deviceSigil: sigil, + drive: drive, + endingAt: newEnd, + relativeBegin: newEnd) + } + + _ = lexer.eatBackslash() + + return .device( + deviceSigil: sigil, + drive: drive, + endingAt: rootEnd, relativeBegin: lexer.current) + } + + internal func _parseWindowsRoot() -> ( + rootEnd: _SystemString.Index, + relativeBegin: _SystemString.Index + ) { + guard let parsed = _parseWindowsRootInternal() else { + return (startIndex, startIndex) + } + return (parsed.rootEnd, parsed.relativeBegin) + } +} + +// Returns the drive letter if `slice` is exactly the 2-byte drive form +// ``, else nil. Intended for testing already- +// extracted device slices; for parsing-from-stream, see `_Lexer.eatDrive`. +@available(SwiftStdlib 9999, *) +private func _driveLetter( + of slice: Slice<_SystemString> +) -> FilePath.CodeUnit? { + var s = slice + guard let first = s._eat(if: { !_isSeparator($0) }), + s._eat(._colon) != nil, + s.isEmpty + else { return nil } + return first +} + +// MARK: - Windows root prenormalization + +@available(SwiftStdlib 9999, *) +extension _SystemString { + internal mutating func _prenormalizeWindowsRoots() -> Index { + _internalInvariant(_isWindows) + + var lexer = _Lexer(self) + + guard lexer.eatBackslash(), lexer.eatBackslash() else { + return lexer.current + } + + // Three or more leading backslashes: NOT a UNC/device path. + // Return after the first backslash; coalescing handles the rest. + if !lexer.isEmpty && lexer.slice.first == ._backslash { + return self.index(after: self.startIndex) + } + + func expectBackslash() { + if lexer.eatBackslash() { return } + let idx = lexer.current + lexer.clear() + self.insert(._backslash, at: idx) + lexer.reset(to: self, at: idx) + let p = lexer.eatBackslash() + _internalInvariant(p) + } + func expectComponent() { + _ = lexer.eatComponent() + expectBackslash() + } + + if let sigil = lexer.eatSigil() { + expectBackslash() + // UNC sub-form only for verbatim (\\?\UNC\...), not device (\\.\UNC\...) + if sigil == ._question, lexer.eatUNC() { + expectBackslash() + expectComponent() // server: its separator is structural + // Share: consume a trailing separator only if one is actually + // present — never synthesize one. The share can be the final + // element of a verbatim-UNC root with no trailing separator + // (\\?\UNC\s\h); forcing a backslash here would store a phantom + // trailing separator and make it compare equal to \\?\UNC\s\h\. + // Mirrors parseUNC's `_ = lexer.eatBackslash()` and the + // `!lexer.isEmpty`-guarded device path. + _ = lexer.eatComponent() + _ = lexer.eatBackslash() + return lexer.current + } + // Check for drive letter device: \\.\C:\ or \\?\C:\. A drive + // letter is any single non-separator code unit before the colon. + let deviceRange = lexer.eatComponent() + if _driveLetter(of: self[deviceRange]) != nil { + // Eat the trailing backslash if present (both branches return + // `lexer.current`). + _ = lexer.eatBackslash() + return lexer.current + } + // Only expect trailing backslash if there's more content + if !deviceRange.isEmpty && !lexer.isEmpty { + expectBackslash() + } + return lexer.current + } + + expectComponent() + return lexer.current + } +} diff --git a/Sources/System/Util.swift b/Sources/System/Util.swift index e4832ac5..689c1660 100644 --- a/Sources/System/Util.swift +++ b/Sources/System/Util.swift @@ -121,11 +121,6 @@ where C.Element: Equatable { } extension MutableCollection where Element: Equatable { - mutating func _replaceAll(_ e: Element, with new: Element) { - for idx in self.indices { - if self[idx] == e { self[idx] = new } - } - } } internal func _withOptionalUnsafePointerOrNull( diff --git a/Sources/System/UtilConsumers.swift b/Sources/System/UtilConsumers.swift index 6977543f..d919a476 100644 --- a/Sources/System/UtilConsumers.swift +++ b/Sources/System/UtilConsumers.swift @@ -10,38 +10,11 @@ // TODO: Below should return an optional of what was eaten extension Slice where Element: Equatable { - internal mutating func _eat(if p: (Element) -> Bool) -> Element? { - guard let s = self.first, p(s) else { return nil } - self = self.dropFirst() - return s - } - internal mutating func _eat(_ e: Element) -> Element? { - _eat(if: { $0 == e }) - } - internal mutating func _eat(asserting e: Element) { let p = _eat(e) assert(p != nil) } - internal mutating func _eat(count c: Int) -> Slice { - defer { self = self.dropFirst(c) } - return self.prefix(c) - } - - internal mutating func _eatSequence(_ es: C) -> Slice? - where C.Element == Element - { - guard self.starts(with: es) else { return nil } - return _eat(count: es.count) - } - - internal mutating func _eatUntil(_ idx: Index) -> Slice { - precondition(idx >= startIndex && idx <= endIndex) - defer { self = self[idx...] } - return self[.. Slice { precondition(idx >= startIndex && idx <= endIndex) guard idx != endIndex else { @@ -63,11 +36,4 @@ extension Slice where Element: Equatable { guard let idx = self.firstIndex(of: e) else { return nil } return _eatThrough(idx) } - - // Eat any elements from the front matching the predicate - internal mutating func _eatWhile(_ p: (Element) -> Bool) -> Slice? { - let idx = firstIndex(where: { !p($0) }) ?? endIndex - guard idx != startIndex else { return nil } - return _eatUntil(idx) - } }