Skip to content

Draft: FilePath.ComponentView #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ let targets: [PackageDescription.Target] = [
path: "Sources/System"),
.target(
name: "SystemInternals",
dependencies: ["CSystem"]),
dependencies: ["CSystem"],
swiftSettings: [
.define("ENABLE_MOCKING")
]),
.target(
name: "CSystem",
dependencies: []),
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ try fd.closeAfter {

## Adding `SystemPackage` as a Dependency

To use the `SystemPackage` library in a SwiftPM project,
To use the `SystemPackage` library in a SwiftPM project,
add the following line to the dependencies in your `Package.swift` file:

```swift
Expand Down
7 changes: 6 additions & 1 deletion Sources/System/FilePath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ extension UnsafeBufferPointer where Element == CChar {
/// like case insensitivity, Unicode normalization, and symbolic links.
// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
public struct FilePath {
internal var bytes: [CChar]
internal typealias Storage = [CChar]
internal var bytes: Storage

/// Creates an empty, null-terminated path.
public init() {
Expand Down Expand Up @@ -186,6 +187,9 @@ extension String {
}
self = str
}

// TODO: Consider a init?(validating:), keeping the encoding agnostic in API and
// dependent on file system.
}

// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
Expand All @@ -205,6 +209,7 @@ extension FilePath: Hashable, Codable {}
extension FilePath {
fileprivate func _invariantCheck() {
precondition(bytes.last! == 0)
// TODO: Should this be a hard trap?
_debugPrecondition(bytes.firstIndex(of: 0) == bytes.count - 1)
}
}
345 changes: 345 additions & 0 deletions Sources/System/FilePathComponents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
/*
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
*/

// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
extension FilePath {
public struct Component: Hashable {
// NOTE: For now, we store a slice of FilePath's storage representation. We'd like to
// have a small-slice representation in the future since the majority of path
// components would easily fit in the 3 words of storage.
//
internal var slice: FilePath.Storage.SubSequence

// TODO: It would be nice to have a ComponentKind. Prefix (Windows only)
// is an important piece of information that has to be parsed from the
// front of the path.

internal init(_ slice: FilePath.Storage.SubSequence) {
self.slice = slice
self.invariantCheck()
}
}
public struct ComponentView {
internal var path: FilePath
}

public var components: ComponentView {
get { ComponentView(path: self) }
set { self = newValue.path }
}
}

// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
extension FilePath.Component {
// WARNING: Return value is dependent on self
fileprivate var unsafeCChars: UnsafeBufferPointer<CChar> {
// Array must implement wCSIA with stable address...
// TODO: A stable address byte buffer (and slice) would work better here...
slice.withContiguousStorageIfAvailable { $0 }!
}

// WARNING: Return value is dependent on self
fileprivate var unsafeUInt8s: UnsafeBufferPointer<UInt8> {
unsafeCChars._asUInt8
}

fileprivate var count: Int { slice.count }

public var isRoot: Bool {
if isSeparator(slice.first!) {
assert(count == 1)
return true
}
return false
}

// TODO: ensure this all gets easily optimized away in release...
fileprivate func invariantCheck() {
defer { _fixLifetime(self) }

// TODO: should this be a debugPrecondition? One can make a component
// explicitly from a string, or maybe it should be a hard precondition
// inside the EBSL init and a assert/debug one here...
assert(isRoot || unsafeCChars.allSatisfy { !isSeparator($0) } )

// TODO: Are we forbidding interior null?
assert(unsafeUInt8s.isEmpty || unsafeUInt8s.last != 0)
}
}

// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
extension String {
/// Creates a string by interpreting the path component's content as UTF-8.
///
/// - Parameter component: The path component to be interpreted as UTF-8.
///
/// If the content of the path component
/// isn't a well-formed UTF-8 string,
/// this initializer removes invalid bytes or replaces them with U+FFFD.
/// This means that, depending on the semantics of the specific file system,
/// conversion to a string and back to a path
/// might result in a value that's different from the original path.
public init(decoding component: FilePath.Component) {
defer { _fixLifetime(component) }
self.init(decoding: component.unsafeUInt8s, as: UTF8.self)
}

/// Creates a string from a path component, validating its UTF-8 contents.
///
/// - Parameter component: The path component to be interpreted as UTF-8.
///
/// If the contents of the path component
/// isn't a well-formed UTF-8 string,
/// this initializer returns `nil`.
public init?(validatingUTF8 component: FilePath.Component) {
// TODO: use a failing initializer for String when one is added...
defer { _fixLifetime(component) }
let str = String(decoding: component)
guard str.utf8.elementsEqual(component.unsafeUInt8s) else { return nil }
self = str
}

// TODO: Consider a init?(validating:), keeping the encoding agnostic in API and
// dependent on file system.
}


// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
extension FilePath.Component: CustomStringConvertible, CustomDebugStringConvertible {

/// A textual representation of the path component.
@inline(never)
public var description: String { String(decoding: self) }

/// A textual representation of the path component, suitable for debugging.
public var debugDescription: String { self.description.debugDescription }
}

// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
extension FilePath.Component: ExpressibleByStringLiteral {
// TODO: Invariant that there's only one component...
// Should we even do this, or rely on FilePath from a literal and overloads?
//
public init(stringLiteral: String) {
self.init(stringLiteral)
}

// TODO: Invariant that there's only one component...
// Should we even do this, or rely on FilePath from a literal and overloads?
//
public init(_ string: String) {
let path = FilePath(string)
precondition(path.components.count == 1)
self = path.components.first!
self.invariantCheck()
}
}


private var canonicalSeparator: CChar { Int8(bitPattern: UInt8(ascii: "/")) }

// TODO: For Windows, this becomes a little more complicated...
private func isSeparator(_ c: CChar) -> Bool { c == canonicalSeparator }


// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
private func separatedComponentBytes<C: Collection>(
_ components: C, addLeadingSeparator: Bool = false, addTrailingSeparator: Bool = false
) -> Array<CChar> where C.Element == FilePath.Component {
var result = addLeadingSeparator ? [canonicalSeparator] : []
defer { _fixLifetime(components) }
let normalized = components.lazy.filter { !$0.isRoot }.map { $0.unsafeCChars }.joined(separator: [canonicalSeparator])
result.append(contentsOf: normalized)

if addTrailingSeparator && (result.isEmpty || !isSeparator(result.last!)) {
result.append(canonicalSeparator)
}
return result
}

// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
extension FilePath.ComponentView: BidirectionalCollection {
public typealias Element = FilePath.Component
public struct Index: Comparable, Hashable {
internal typealias Storage = FilePath.Storage.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(path.bytes.startIndex) }

// Use the index of the guaranteed null terminator
public var endIndex: Index { Index(path.bytes.indices.last!) }

// Find the end of the component starting at `i`.
private func parseComponentEnd(startingAt i: Index.Storage) -> Index.Storage {
if isSeparator(path.bytes[i]) {
// Special case: leading separator signifies root
assert(i == path.bytes.startIndex)
return path.bytes.index(after: i)
}

return path.bytes[i...].firstIndex(where: { isSeparator($0) }) ?? endIndex._storage
}

// Find the start of the component after the end of the prior at `i`
private func parseNextComponentStart(
afterComponentEnd i: Index.Storage
) -> Index.Storage {
assert(i != endIndex._storage)
if !isSeparator(path.bytes[i]) {
assert(i == path.bytes.index(after: path.bytes.startIndex))
// TODO: what about when we're done parsing and we have null terminator?
}
return path.bytes[i...].firstIndex(where: { !isSeparator($0) }) ?? endIndex._storage
}

public func index(after i: Index) -> Index {
let end = parseComponentEnd(startingAt: i._storage)
if Index(end) == endIndex {
return endIndex
}
return Index(parseNextComponentStart(afterComponentEnd: end))
}

// Find the start of the component prior to the after the end of the prior at `i`
private func parseComponentStart(
endingAt i: Index.Storage
) -> Index.Storage {
assert(i != startIndex._storage)

return path.bytes[i...].firstIndex(where: { !isSeparator($0) }) ?? startIndex._storage
}

// Chew through separators until we get to a component end
private func parseComponentEnd(fromStart i: Index) -> Index.Storage {
let slice = path.bytes[..<i._storage]
return slice.lastIndex(where: { isSeparator($0) }) ?? startIndex._storage
}

public func index(before i: Index) -> Index {
var slice = path.bytes[..<i._storage]
while let c = slice.last, isSeparator(c) {
slice.removeLast()
}
while let c = slice.last, !isSeparator(c) {
slice.removeLast()
}

return Index(slice.endIndex)
}

public subscript(position: Index) -> FilePath.Component {
let i = position
let end = parseComponentEnd(startingAt: i._storage)
return FilePath.Component(path.bytes[i._storage ..< end])
}
}

// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
extension FilePath.ComponentView: RangeReplaceableCollection {
public init() {
self.init(path: FilePath())
}

public mutating func replaceSubrange<C: Collection>(
_ subrange: Range<Index>, with newElements: C
) where Element == C.Element {
let (lowerBound, upperBound) = (subrange.lowerBound, subrange.upperBound)

let pathRange = lowerBound._storage ..< upperBound._storage
guard !newElements.isEmpty else {
path.bytes.removeSubrange(pathRange)
return
}

// Insertion skips roots
let hasNewComponents = !newElements.lazy.filter { !$0.isRoot }.isEmpty

// Explicitly add a trailing separator if
// not at end and next character is not a separator
let atEnd = upperBound == endIndex
let trailingSeparator = !atEnd && !isSeparator(path.bytes[upperBound._storage])

// Explicitly add a preceding separator if
// replacing front with absolute components (unless redundant by trailing separator),
// preceding character is not a separator (which implies at the end)
let atStart = lowerBound == startIndex
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Account for // special case...

Multiple successive characters are considered to be the same as one , except for the case of exactly two leading characters.

https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_271

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For POSIX, it seems to denote implementation defined path resolution:

A pathname consisting of a single shall resolve to the root directory of the process. A null pathname shall not be successfully resolved. If a pathname begins with two successive characters, the first component following the leading characters may be interpreted in an implementation-defined manner, although more than two leading characters shall be treated as a single character.

https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13

Copy link
Member

@lorentey lorentey Sep 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Luckily, all signs indicate that // is special cased on neither Darwin nor Linux, so there is no need for us to do anything about it. If so, it's okay to treat // as a synonym of / for the purposes of canonicalization and component extraction.

let componentsAreAbsolute = newElements.first!.isRoot
let leadingSeparator: Bool
if atStart {
leadingSeparator = componentsAreAbsolute && (path.isRelative || hasNewComponents)
} else if !isSeparator(path.bytes[path.bytes.index(before: lowerBound._storage)]) {
assert(lowerBound == endIndex) // precondition?
leadingSeparator = hasNewComponents
} else {
leadingSeparator = false
}

let newBytes = separatedComponentBytes(
newElements,
addLeadingSeparator: leadingSeparator,
addTrailingSeparator: trailingSeparator)

path.bytes.replaceSubrange(pathRange, with: newBytes)
}
}


// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
extension FilePath {
public init<C: Collection>(_ components: C) where C.Element == Component {
self.init(byteContents: separatedComponentBytes(components, addLeadingSeparator: components.first?.isRoot ?? false))
}

// FIXME: Include `~` as an absolute path first component
public var isAbsolute: Bool { components.first?.isRoot ?? false }

public var isRelative: Bool { !isAbsolute }

public mutating func append(_ other: FilePath) {
// TODO: We can do a faster byte copy operation, after checking
// for leading/trailing slashes...
self.components.append(contentsOf: other.components)
}

public static func +(_ lhs: FilePath, _ rhs: FilePath) -> FilePath {
var result = lhs
result.append(rhs)
return result
}

/* TODO:
public mutating func push(_ component: FilePath.Component) {
}
public mutating func push(_ path: FilePath) {
}
public mutating func push<C: Collection>(
contentsOf components: C
) where C.Element == FilePath.Component {
}

@discardableResult
public mutating func pop() -> FilePath.Component? {
... or should this trap if empty?
}
@discardableResult
public mutating func pop(_ n: Int) -> FilePath.Component? {
... or should this trap if empty?
}
*/

}

Loading