| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538 |
- /*
- 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
- */
- internal struct _ParsedWindowsRoot {
- var rootEnd: SystemString.Index
- // TODO: Remove when I normalize to always (except `C:`)
- // have trailing separator
- var relativeBegin: SystemString.Index
- var drive: SystemChar?
- var fullyQualified: Bool
- var deviceSigil: SystemChar?
- var host: Range<SystemString.Index>?
- var volume: Range<SystemString.Index>?
- }
- 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<SystemString.Index>,
- share: Range<SystemString.Index>,
- 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<SystemString.Index>,
- 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<SystemString>
- 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<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() {
- // 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<Result>(_ 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..<dwLength).contains(GetFullPathNameW(self, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else {
- throw Errno(rawValue: _mapWindowsErrorToErrno(GetLastError()))
- }
- // 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these.
- if let base = pwszFullPath.baseAddress,
- base[0] == UInt8(ascii: "\\"),
- base[1] == UInt8(ascii: "\\"),
- base[2] == UInt8(ascii: "."),
- base[3] == UInt8(ascii: "\\") {
- return try body(base)
- }
- // 2. Canonicalize the path.
- // This will add the \\?\ prefix if needed based on the path's length.
- var pwszCanonicalPath: LPWSTR?
- let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue)
- let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath)
- if let pwszCanonicalPath {
- defer { LocalFree(pwszCanonicalPath) }
- if result == S_OK {
- // 3. Perform the operation on the normalized path.
- return try body(pwszCanonicalPath)
- }
- }
- throw Errno(rawValue: _mapWindowsErrorToErrno(WIN32_FROM_HRESULT(result)))
- }
- }
- }
- @inline(__always)
- fileprivate func HRESULT_CODE(_ hr: HRESULT) -> 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
|