mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-16 01:42:23 +08:00
Compare commits
10 Commits
219000b45b
...
2292b40ccd
Author | SHA1 | Date | |
---|---|---|---|
|
2292b40ccd | ||
|
fa43c96f5f | ||
|
2f65d91455 | ||
|
117041b61f | ||
|
b95199858b | ||
|
a95c2c8d43 | ||
|
2ea76c322f | ||
|
4c021b6e57 | ||
|
6cde050fb3 | ||
|
3c9559d68a |
Proposals
Sources/FoundationEssentials
AttributedString
AttributeContainer.swiftAttributedString+Runs+Run.swiftAttributedString.swiftAttributedSubstring.swiftDiscontiguousAttributedSubstring.swift
Calendar
Data
FileManager
Formatting
JSON
String
Tests/FoundationEssentialsTests
256
Proposals/00020-uri-templating.md
Normal file
256
Proposals/00020-uri-templating.md
Normal file
@ -0,0 +1,256 @@
|
||||
# URI Templating
|
||||
|
||||
* Proposal: [SF-00020](00020-uri-templating.md)
|
||||
* Authors: [Daniel Eggert](https://github.com/danieleggert)
|
||||
* Review Manager: [Tina L](https://github.com/itingliu)
|
||||
* Status: **Review: March 14, 2025...March 21, 2025**
|
||||
* Implementation: [swiftlang/swift-foundation#1198](https://github.com/swiftlang/swift-foundation/pull/1198)
|
||||
* Review: ([pitch](https://forums.swift.org/t/pitch-uri-templating/78030))
|
||||
|
||||
## Introduction
|
||||
|
||||
This proposal adds support for [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) _URI templates_ to the Swift URL type.
|
||||
|
||||
Although there are multiple levels of expansion, the core concept is that you can define a _template_ such as
|
||||
```
|
||||
http://example.com/~{username}/
|
||||
http://example.com/dictionary/{term:1}/{term}
|
||||
http://example.com/search{?q,lang}
|
||||
```
|
||||
|
||||
and then _expand_ these using named values (i.e. a dictionary) into a `URL`.
|
||||
|
||||
The templating has a rich set of options for substituting various parts of URLs. [RFC 6570 section 1.2](https://datatracker.ietf.org/doc/html/rfc6570#section-1.2) lists all 4 levels of increasing complexity.
|
||||
|
||||
## Motivation
|
||||
|
||||
[RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) provides a simple, yet powerful way to allow for variable expansion in URLs.
|
||||
|
||||
This provides a mechanism for a server to convey to clients how to construct URLs for specific resources. In the [RFC 8620 JMAP protocol](https://datatracker.ietf.org/doc/html/rfc8620) for example, the server sends it client a template such as
|
||||
```
|
||||
https://jmap.example.com/download/{accountId}/{blobId}/{name}?accept={type}
|
||||
```
|
||||
and the client can then use variable expansion to construct a URL for resources. The API contract between the server and the client defines which variables this specific template has, and which ones are optional.
|
||||
|
||||
Since URI templates provide a powerful way to define URL patterns with placeholders, they are adopted in various standards.
|
||||
|
||||
## Proposed solution
|
||||
|
||||
```swift
|
||||
guard
|
||||
let template = URL.Template("http://www.example.com/foo{?query,number}"),
|
||||
let url = URL(
|
||||
template: template,
|
||||
variables: [
|
||||
"query": "bar baz",
|
||||
"number": "234",
|
||||
]
|
||||
)
|
||||
else { return }
|
||||
```
|
||||
|
||||
The RFC 6570 template gets parsed as part of the `URL.Template(_:)` initializer. It will return `nil` if the passed in string is not a valid template.
|
||||
|
||||
The `Template` can then be expanded with _variables_ to create a URL:
|
||||
```swift
|
||||
extension URL {
|
||||
public init?(
|
||||
template: URL.Template,
|
||||
variables: [URL.Template.VariableName: URL.Template.Value]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Detailed design
|
||||
|
||||
### Templates and Expansion
|
||||
|
||||
[RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) defines 8 different kinds of expansions:
|
||||
* Simple String Expansion: `{var}`
|
||||
* Reserved Expansion: `{+var}`
|
||||
* Fragment Expansion: `{#var}`
|
||||
* Label Expansion with Dot-Prefix: `{.var}`
|
||||
* Path Segment Expansion: `{/var}`
|
||||
* Path-Style Parameter Expansion: `{;var}`
|
||||
* Form-Style Query Expansion: `{?var}`
|
||||
* Form-Style Query Continuation: `{&var}`
|
||||
|
||||
Additionally, RFC 6570 allows for _prefix values_ and _composite values_.
|
||||
|
||||
Prefix values allow for e.g. `{var:3}` which would result in (up to) the first 3 characters of `var`.
|
||||
|
||||
Composite values allow `/mapper{?address*}` to e.g. expand into `/mapper?city=Newport%20Beach&state=CA`.
|
||||
|
||||
This implementation covers all levels and expression types defined in the RFC.
|
||||
|
||||
### API Details
|
||||
|
||||
There are 3 new types:
|
||||
* `URL.Template`
|
||||
* `URL.Template.VariableName`
|
||||
* `URL.Template.Value`
|
||||
|
||||
All new API is guarded by `@available(FoundationPreview 6.2, *)`.
|
||||
|
||||
#### `Template`
|
||||
|
||||
`URL.Template` represents a parsed template that can be used to create a `URL` from it by _expanding_ variables according to RFC 6570.
|
||||
|
||||
Its sole API is its initializer:
|
||||
```swift
|
||||
extension URL {
|
||||
/// A template for constructing a URL from variable expansions.
|
||||
///
|
||||
/// This is an template that can be expanded into
|
||||
/// a ``URL`` by calling ``URL(template:variables:)``.
|
||||
///
|
||||
/// Templating has a rich set of options for substituting various parts of URLs. See
|
||||
/// [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) for
|
||||
/// details.
|
||||
public struct Template: Sendable, Hashable {}
|
||||
}
|
||||
|
||||
extension URL.Template {
|
||||
/// Creates a new template from its text form.
|
||||
///
|
||||
/// The template string needs to be a valid RFC 6570 template.
|
||||
///
|
||||
/// If parsing the template fails, this will return `nil`.
|
||||
public init?(_ template: String)
|
||||
}
|
||||
```
|
||||
|
||||
It will return `nil` if the provided string can not be parsed as a valid template.
|
||||
|
||||
#### Variables
|
||||
|
||||
Variables are represented as a `[URL.Template.VariableName: URL.Template.Value]`.
|
||||
|
||||
#### `VariableName`
|
||||
|
||||
The `URL.Template.VariableName` type is a type-safe wrapper around `String`
|
||||
```swift
|
||||
extension URL.Template {
|
||||
/// The name of a variable used for expanding a template.
|
||||
public struct VariableName: Sendable, Hashable {
|
||||
public init(_ key: String)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The following extensions and conformances make it easy to convert between `VariableName` and `String`:
|
||||
```swift
|
||||
extension String {
|
||||
public init(_ key: URL.Template.VariableName)
|
||||
}
|
||||
|
||||
extension URL.Template.VariableName: CustomStringConvertible {
|
||||
public var description: String
|
||||
}
|
||||
|
||||
extension URL.Template.VariableName: ExpressibleByStringLiteral {
|
||||
public init(stringLiteral value: String)
|
||||
}
|
||||
```
|
||||
|
||||
#### `Value`
|
||||
|
||||
The `URL.Template.Value` type can represent the 3 different kinds of values that RFC 6570 supports:
|
||||
```swift
|
||||
extension URL.Template {
|
||||
/// The value of a variable used for expanding a template.
|
||||
///
|
||||
/// A ``Value`` can be one of 3 kinds:
|
||||
/// 1. "text": a single `String`
|
||||
/// 2. "list": an array of `String`
|
||||
/// 3. "associative list": an ordered array of key-value pairs of `String` (similar to `Dictionary`, but ordered).
|
||||
public struct Value: Sendable, Hashable {}
|
||||
}
|
||||
|
||||
extension URL.Template.Value {
|
||||
/// A text value to be used with a ``URL.Template``.
|
||||
public static func text(_ text: String) -> URL.Template.Value
|
||||
|
||||
/// A list value (an array of `String`s) to be used with a ``URL.Template``.
|
||||
public static func list(_ list: some Sequence<String>) -> URL.Template.Value
|
||||
|
||||
/// An associative list value (ordered key-value pairs) to be used with a ``URL.Template``.
|
||||
public static func associativeList(_ list: some Sequence<(key: String, value: String)>) -> URL.Template.Value
|
||||
}
|
||||
```
|
||||
|
||||
To make it easier to use hard-coded values, the following `ExpressibleBy…` conformances are provided:
|
||||
```swift
|
||||
extension URL.Template.Value: ExpressibleByStringLiteral {
|
||||
public init(stringLiteral value: String)
|
||||
}
|
||||
|
||||
extension URL.Template.Value: ExpressibleByArrayLiteral {
|
||||
public init(arrayLiteral elements: String...)
|
||||
}
|
||||
|
||||
extension URL.Template.Value: ExpressibleByDictionaryLiteral {
|
||||
public init(dictionaryLiteral elements: (String, String)...)
|
||||
}
|
||||
```
|
||||
|
||||
#### Expansion to `URL`
|
||||
|
||||
Finally, `URL.Template` has this factory method:
|
||||
```swift
|
||||
extension URL {
|
||||
/// Creates a new `URL` by expanding the RFC 6570 template and variables.
|
||||
///
|
||||
/// This will fail if variable expansion does not produce a valid,
|
||||
/// well-formed URL.
|
||||
///
|
||||
/// All text will be converted to NFC (Unicode Normalization Form C) and UTF-8
|
||||
/// before being percent-encoded if needed.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - template: The RFC 6570 template to be expanded.
|
||||
/// - variables: Variables to expand in the template.
|
||||
public init?(
|
||||
template: URL.Template,
|
||||
variables: [URL.Template.VariableName: URL.Template.Value]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This will only fail (return `nil`) if `URL.init?(string:)` fails.
|
||||
|
||||
It may seem counterintuitive when and how this could fail, but a string such as `http://example.com:bad%port/` would cause `URL.init?(string:)` to fail, and URI Templates do not provide a way to prevent this. It is also worth noting that it is valid to not provide values for all variables in the template. Expansion will still succeed, generating a string. If this string is a valid URL, depends on the exact details of the template. Determining which variables exist in a template, which are required for expansion, and whether the resulting URL is valid is part of the API contract between the server providing the template and the client generating the URL.
|
||||
|
||||
Additionally, the new types `URL.Template`, `URL.Template.VariableName`, and `URL.Template.Value` all conform to `CustomStringConvertible`.
|
||||
|
||||
### Unicode
|
||||
|
||||
The _expansion_ that happens as part of calling `URL(template:variables:)` will
|
||||
* convert text to NFC (Unicode Normalization Form C)
|
||||
* convert text to UTF-8 before being percent-encoded (if needed).
|
||||
|
||||
as per [RFC 6570 section 1.6](https://datatracker.ietf.org/doc/html/rfc6570#section-1.6).
|
||||
|
||||
## Source compatibility
|
||||
|
||||
These changes are additive only and are not expected to have an impact on source compatibility.
|
||||
|
||||
## Implications on adoption
|
||||
|
||||
This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility.
|
||||
|
||||
## Future directions
|
||||
|
||||
Since this proposal covers all of RFC 6570, the current expectation is for it to not be extended further.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Instead of `URL.init?(template:variables)`, the API could have used a method on `URL.Template`, e.g. `URL.Template.makeURL(variables:)`. The `URL.init?` approach would be less discoverable. There was some feedback to the initial pitch, though, that preferred the `URL.init?` method which aligns with the existing `URL.init?(string:)` initializer. This initializer approach aligns more with existing `URL.init?(string:)` and `String.init(format:)`.
|
||||
|
||||
Additionally, the API _could_ expose a (non-failing!) `URL.Template.expand(variables:)` (or other naming) that returns a `String`. But since the purpose is very clearly to create URLs, it feels like that would just add noise.
|
||||
|
||||
Using a DSL (domain-specific language) for `URL.Template` could improve type safety. However, because servers typically send templates as strings for client-side processing and request generation, the added complexity of a DSL outweighs its benefits. The proposed implementation is string-based (_stringly typed_) because that is what the RFC 6570 mandates.
|
||||
|
||||
There was a lot of interest during the pitch to have an API that lends itself to _routing_, providing a way to go back-and-forth between a route and its variables. But that’s a very different use case than the RFC 6570 templates provide, and it would be better suited to have a web server routing specific API, either in Foundation or in a web server specific package. [pointfreeco/swift-url-routing](https://github.com/pointfreeco/swift-url-routing) is one such example.
|
||||
|
||||
Instead of using the `text`, `list` and `associativeList` names (which are _terms of art_ in RFC 6570), the names `string`, `array`, and `orderedDictioary` would align better with normal Swift naming conventions. The proposal favors the _terms of art_, but there was some interest in using changing this.
|
212
Proposals/0021-ISO8601ComponentsStyle.md
Normal file
212
Proposals/0021-ISO8601ComponentsStyle.md
Normal file
@ -0,0 +1,212 @@
|
||||
# ISO8601 Components Formatting and Parsing
|
||||
|
||||
* Proposal: SF-0021
|
||||
* Author(s): Tony Parker <anthony.parker@apple.com>
|
||||
* Status: **Review: March 17, 2025...March 24, 2025**
|
||||
* Intended Release: _Swift 6.2_
|
||||
* Review: ([pitch](https://forums.swift.org/t/pitch-iso8601-components-format-style/77990))
|
||||
*_Related issues_*
|
||||
|
||||
* https://github.com/swiftlang/swift-foundation/issues/323
|
||||
* https://github.com/swiftlang/swift-foundation/issues/967
|
||||
* https://github.com/swiftlang/swift-foundation/issues/1159
|
||||
|
||||
## Revision history
|
||||
|
||||
* **v1** Initial version
|
||||
|
||||
## Introduction
|
||||
|
||||
Based upon feedback from adoption of `ISO8601FormatStyle`, we propose two changes and one addition to the API:
|
||||
|
||||
- Change the behavior of the `includingFractionalSeconds` flag with respect to parsing. `ISO8601FormatStyle` will now always allow fractional seconds regardless of the setting.
|
||||
- Change the behavior of the time zone flag with respect to parsing. `ISO8601FormatStyle` will now always allow hours-only time zone offsets.
|
||||
- Add a _components_ style, which formats `DateComponents` into ISO8601 and parses ISO8601-formatted `String`s into `DateComponents`.
|
||||
|
||||
## Motivation
|
||||
|
||||
The existing `Date.ISO8601FormatStyle` type has one property for controlling fractional seconds, and it is also settable in the initializer. The existing behavior is that parsing _requires_ presence of fractional seconds if set, and _requires_ absence of fractional seconds if not set.
|
||||
|
||||
If the caller does not know if the string contains the fractional seconds or not, they are forced to parse the string twice:
|
||||
|
||||
```swift
|
||||
let str = "2022-01-28T15:35:46Z"
|
||||
var result = try? Date.ISO8601FormatStyle(includingFractionalSeconds: false).parse(str)
|
||||
if result == nil {
|
||||
result = try? Date.ISO8601FormatStyle(includingFractionalSeconds: true).parse(str)
|
||||
}
|
||||
```
|
||||
|
||||
In most cases, the caller simply does not care if the fractional seconds are present or not. Therefore, we propose changing the behavior of the parser to **always** allow fractional seconds, regardless of the setting of the `includeFractionalSeconds` flag. The flag is still used for formatting.
|
||||
|
||||
With respect to time zone offsets, the parser has always allowed the optional presence of seconds, as well the optional presence of `:`. We propose extending this behavior to allow optional minutes as well. The following are considered well-formed by the parser:
|
||||
|
||||
```
|
||||
2022-01-28T15:35:46 +08
|
||||
2022-01-28T15:35:46 +0800
|
||||
2022-01-28T15:35:46 +080000
|
||||
2022-01-28T15:35:46 +08
|
||||
2022-01-28T15:35:46 +08:00
|
||||
2022-01-28T15:35:46 +08:00:00
|
||||
```
|
||||
|
||||
In order to provide an alternative for cases where strict parsing is required, a new parser is provided that returns the _components_ of the parsed date instead of the resolved `Date` itself. This new parser also provides a mechanism to retrieve the time zone from an ISO8601-formatted string. Following parsing of the components, the caller can resolve them into a `Date` using the regular `Calendar` and `DateComponents` API.
|
||||
|
||||
## Proposed solution and example
|
||||
|
||||
In addition to the behavior change above, we propose introducing a new `DateComponents.ISO8601FormatStyle`. The API surface is nearly identical to `Date.ISO8601FormatStyle`, with the exception of the output type. It reuses the same inner types, and they share a common implementation. The full API surface is in the detailed design, below.
|
||||
|
||||
Formatting ISO8601 components is just as straightforward as formatting a `Date`.
|
||||
|
||||
```swift
|
||||
let components = DateComponents(year: 1999, month: 12, day: 31, hour: 23, minute: 59, second: 59)
|
||||
let formatted = components.formatted(.iso8601Components)
|
||||
print(formatted) // 1999-12-31T23:59:59Z
|
||||
```
|
||||
|
||||
Parsing ISO8601 components follows the same pattern as other parse strategies:
|
||||
|
||||
```swift
|
||||
let components = try DateComponents.ISO8601FormatStyle().parse("2022-01-28T15:35:46Z")
|
||||
// components are: DateComponents(timeZone: .gmt, year: 2022, month: 1, day: 28, hour: 15, minute: 35, second: 46))
|
||||
```
|
||||
|
||||
If further conversion to a `Date` is required, the existing `Calendar` API can be used:
|
||||
|
||||
```swift
|
||||
let date = components.date // optional result, date may be invalid
|
||||
```
|
||||
|
||||
## Detailed design
|
||||
|
||||
The full API surface of the new style is:
|
||||
|
||||
```swift
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents {
|
||||
/// Options for generating and parsing string representations of dates following the ISO 8601 standard.
|
||||
public struct ISO8601FormatStyle : Sendable, Codable, Hashable {
|
||||
public var timeSeparator: Date.ISO8601FormatStyle.TimeSeparator { get }
|
||||
/// If set, fractional seconds will be present in formatted output. Fractional seconds may be present in parsing regardless of the setting of this property.
|
||||
public var includingFractionalSeconds: Bool { get }
|
||||
public var timeZoneSeparator: Date.ISO8601FormatStyle.TimeZoneSeparator { get }
|
||||
public var dateSeparator: Date.ISO8601FormatStyle.DateSeparator { get }
|
||||
public var dateTimeSeparator: Date.ISO8601FormatStyle.DateTimeSeparator { get }
|
||||
|
||||
public init(from decoder: any Decoder) throws
|
||||
public func encode(to encoder: any Encoder) throws
|
||||
|
||||
public func hash(into hasher: inout Hasher)
|
||||
|
||||
public static func ==(lhs: ISO8601FormatStyle, rhs: ISO8601FormatStyle) -> Bool
|
||||
public var timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!
|
||||
|
||||
// The default is the format of RFC 3339 with no fractional seconds: "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
|
||||
public init(dateSeparator: Date.ISO8601FormatStyle.DateSeparator = .dash, dateTimeSeparator: Date.ISO8601FormatStyle.DateTimeSeparator = .standard, timeSeparator: Date.ISO8601FormatStyle.TimeSeparator = .colon, timeZoneSeparator: Date.ISO8601FormatStyle.TimeZoneSeparator = .omitted, includingFractionalSeconds: Bool = false, timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!)
|
||||
}
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents.ISO8601FormatStyle {
|
||||
public func year() -> Self
|
||||
public func weekOfYear() -> Self
|
||||
public func month() -> Self
|
||||
public func day() -> Self
|
||||
public func time(includingFractionalSeconds: Bool) -> Self
|
||||
public func timeZone(separator: Date.ISO8601FormatStyle.TimeZoneSeparator) -> Self
|
||||
public func dateSeparator(_ separator: Date.ISO8601FormatStyle.DateSeparator) -> Self
|
||||
public func dateTimeSeparator(_ separator: Date.ISO8601FormatStyle.DateTimeSeparator) -> Self
|
||||
public func timeSeparator(_ separator: Date.ISO8601FormatStyle.TimeSeparator) -> Self
|
||||
public func timeZoneSeparator(_ separator: Date.ISO8601FormatStyle.TimeZoneSeparator) -> Self
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents.ISO8601FormatStyle : FormatStyle {
|
||||
public func format(_ value: DateComponents) -> String
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
public extension FormatStyle where Self == DateComponents.ISO8601FormatStyle {
|
||||
static var iso8601Components: Self
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
public extension ParseableFormatStyle where Self == DateComponents.ISO8601FormatStyle {
|
||||
static var iso8601Components: Self
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
public extension ParseStrategy where Self == DateComponents.ISO8601FormatStyle {
|
||||
@_disfavoredOverload
|
||||
static var iso8601Components: Self
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents.ISO8601FormatStyle : ParseStrategy {
|
||||
public func parse(_ value: String) throws -> DateComponents
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents.ISO8601FormatStyle: ParseableFormatStyle {
|
||||
public var parseStrategy: Self
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents.ISO8601FormatStyle : CustomConsumingRegexComponent {
|
||||
public typealias RegexOutput = DateComponents
|
||||
public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: DateComponents)?
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension RegexComponent where Self == DateComponents.ISO8601FormatStyle {
|
||||
@_disfavoredOverload
|
||||
public static var iso8601Components: DateComponents.ISO8601FormatStyle
|
||||
|
||||
public static func iso8601ComponentsWithTimeZone(includingFractionalSeconds: Bool = false, dateSeparator: Date.ISO8601FormatStyle.DateSeparator = .dash, dateTimeSeparator: Date.ISO8601FormatStyle.DateTimeSeparator = .standard, timeSeparator: Date.ISO8601FormatStyle.TimeSeparator = .colon, timeZoneSeparator: Date.ISO8601FormatStyle.TimeZoneSeparator = .omitted) -> Self
|
||||
|
||||
public static func iso8601Components(timeZone: TimeZone, includingFractionalSeconds: Bool = false, dateSeparator: Date.ISO8601FormatStyle.DateSeparator = .dash, dateTimeSeparator: Date.ISO8601FormatStyle.DateTimeSeparator = .standard, timeSeparator: Date.ISO8601FormatStyle.TimeSeparator = .colon) -> Self
|
||||
|
||||
public static func iso8601Components(timeZone: TimeZone, dateSeparator: Date.ISO8601FormatStyle.DateSeparator = .dash) -> Self
|
||||
}
|
||||
```
|
||||
|
||||
Unlike the `Date` format style, formatting with a `DateComponents` style can have a mismatch between the specified output fields and the contents of the `DateComponents` struct. In the case where the input `DateComponents` is missing required values, then the formatter will fill in default values to ensure correct output.
|
||||
|
||||
```swift
|
||||
let components = DateComponents(year: 1999, month: 12, day: 31)
|
||||
let formatted = components.formatted(.iso8601Components) // 1999-12-31T00:00:00Z
|
||||
```
|
||||
|
||||
## Impact on existing code
|
||||
|
||||
The change to always allow fractional seconds will affect existing code. As described above, we believe the improvement in the API surface is worth the risk of introducing unexpected behavior for the rare case that a parser truly needs to specify the exact presence or absence of frational seconds.
|
||||
|
||||
If code depending on this new behavior must be backdeployed before Swift 6.2, then Swift's `if #available` checks may be used to parse twice on older releases of the OS or Swift.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### "Allowing" Option
|
||||
|
||||
We considered adding a new flag to `Date.ISO8601FormatStyle` to control the optional parsing of fractional seconds. However, the truth table quickly became confusing:
|
||||
|
||||
#### Formatting
|
||||
|
||||
| `includingFractionalSeconds` | `allowingFractionalSeconds` | Fractional Seconds |
|
||||
| -- | -- | -- |
|
||||
| `true` | `true` | Included |
|
||||
| `true` | `false` | Included |
|
||||
| `false` | `true` | Excluded |
|
||||
| `false` | `true` | Excluded |
|
||||
|
||||
#### Parsing
|
||||
|
||||
| `includingFractionalSeconds` | `allowingFractionalSeconds` | Fractional Seconds |
|
||||
| -- | -- | -- |
|
||||
| `true` | `true` | Required Present |
|
||||
| `true` | `false` | Required Present |
|
||||
| `false` | `true` | ? |
|
||||
| `false` | `true` | Allow Present or Missing |
|
||||
|
||||
In addition, all the initializers needed to be duplicated to add the new option.
|
||||
|
||||
In practice, the additional complexity did not seem worth the tradeoff with the potential for a compatibility issue with the existing style. This does require callers to be aware that the behavior has changed in the release in which this feature ships. Therefore, we will be clear in the documentation about the change.
|
@ -33,6 +33,7 @@ extension AttributeContainer {
|
||||
}
|
||||
|
||||
@preconcurrency
|
||||
@inlinable // Trivial implementation, allows callers to optimize away the keypath allocation
|
||||
public subscript<K: AttributedStringKey>(dynamicMember keyPath: KeyPath<AttributeDynamicLookup, K>) -> K.Value? where K.Value : Sendable {
|
||||
get { self[K.self] }
|
||||
set { self[K.self] = newValue }
|
||||
|
@ -81,6 +81,7 @@ extension AttributedString.Runs.Run {
|
||||
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
||||
extension AttributedString.Runs.Run {
|
||||
@preconcurrency
|
||||
@inlinable // Trivial implementation, allows callers to optimize away the keypath allocation
|
||||
public subscript<K: AttributedStringKey>(
|
||||
dynamicMember keyPath: KeyPath<AttributeDynamicLookup, K>
|
||||
) -> K.Value?
|
||||
|
@ -243,6 +243,7 @@ extension AttributedString: AttributedStringProtocol {
|
||||
}
|
||||
|
||||
@preconcurrency
|
||||
@inlinable // Trivial implementation, allows callers to optimize away the keypath allocation
|
||||
public subscript<K: AttributedStringKey>(
|
||||
dynamicMember keyPath: KeyPath<AttributeDynamicLookup, K>
|
||||
) -> K.Value? where K.Value: Sendable {
|
||||
|
@ -177,6 +177,7 @@ extension AttributedSubstring {
|
||||
}
|
||||
|
||||
@preconcurrency
|
||||
@inlinable // Trivial implementation, allows callers to optimize away the keypath allocation
|
||||
public subscript<K: AttributedStringKey>(
|
||||
dynamicMember keyPath: KeyPath<AttributeDynamicLookup, K>
|
||||
) -> K.Value? where K.Value : Sendable {
|
||||
|
@ -194,6 +194,7 @@ extension DiscontiguousAttributedSubstring {
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable // Trivial implementation, allows callers to optimize away the keypath allocation
|
||||
public subscript<K: AttributedStringKey>(
|
||||
dynamicMember keyPath: KeyPath<AttributeDynamicLookup, K>
|
||||
) -> K.Value? where K.Value : Sendable {
|
||||
|
@ -601,7 +601,14 @@ public struct Calendar : Hashable, Equatable, Sendable {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.date(byAdding: dc, to: date.capped, wrappingComponents: wrappingComponents)
|
||||
let result = self.date(byAdding: dc, to: date.capped, wrappingComponents: wrappingComponents)
|
||||
#if FOUNDATION_FRAMEWORK
|
||||
// Compatibility path - we found some apps depending on the result being non-nil
|
||||
if Calendar.compatibility2 {
|
||||
return result ?? date
|
||||
}
|
||||
#endif
|
||||
return result
|
||||
}
|
||||
|
||||
/// Returns a sequence of `Date`s, calculated by adding a scaled amount of `Calendar.Component`s to a starting `Date`.
|
||||
@ -674,6 +681,19 @@ public struct Calendar : Hashable, Equatable, Sendable {
|
||||
return dc
|
||||
}
|
||||
|
||||
/// Same as `dateComponents:from:` but uses the more efficient bitset form of ComponentSet.
|
||||
/// Prefixed with `_` to avoid ambiguity at call site with the `Set<Component>` method.
|
||||
internal func _dateComponents(_ components: ComponentSet, from date: Date, in timeZone: TimeZone) -> DateComponents {
|
||||
var dc = _calendar.dateComponents(components, from: date.capped, in: timeZone)
|
||||
|
||||
// Fill out the Calendar field of dateComponents, if requested.
|
||||
if components.contains(.calendar) {
|
||||
dc.calendar = self
|
||||
}
|
||||
|
||||
return dc
|
||||
}
|
||||
|
||||
/// Returns all the date components of a date, as if in a given time zone (instead of the `Calendar` time zone).
|
||||
///
|
||||
/// The time zone overrides the time zone of the `Calendar` for the purposes of this calculation.
|
||||
|
@ -55,3 +55,29 @@ extension Date {
|
||||
}
|
||||
#endif // FOUNDATION_FRAMEWORK
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents {
|
||||
/// Converts `self` to its textual representation.
|
||||
/// - Parameter format: The format for formatting `self`.
|
||||
/// - Returns: A representation of `self` using the given `format`. The type of the representation is specified by `FormatStyle.FormatOutput`.
|
||||
public func formatted<F: FormatStyle>(_ format: F) -> F.FormatOutput where F.FormatInput == DateComponents {
|
||||
format.format(self)
|
||||
}
|
||||
|
||||
// Parsing
|
||||
/// Creates a new `DateComponents` by parsing the given representation.
|
||||
/// - Parameter value: A representation of a date. The type of the representation is specified by `ParseStrategy.ParseInput`.
|
||||
/// - Parameters:
|
||||
/// - value: A representation of a date. The type of the representation is specified by `ParseStrategy.ParseInput`.
|
||||
/// - strategy: The parse strategy to parse `value` whose `ParseOutput` is `DateComponents`.
|
||||
public init<T: ParseStrategy>(_ value: T.ParseInput, strategy: T) throws where T.ParseOutput == Self {
|
||||
self = try strategy.parse(value)
|
||||
}
|
||||
|
||||
/// Creates a new `DateComponents` by parsing the given string representation.
|
||||
@_disfavoredOverload
|
||||
public init<T: ParseStrategy, Value: StringProtocol>(_ value: Value, strategy: T) throws where T.ParseOutput == Self, T.ParseInput == String {
|
||||
self = try strategy.parse(String(value))
|
||||
}
|
||||
}
|
||||
|
@ -10,20 +10,10 @@
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Bionic)
|
||||
import Bionic
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#elseif os(Windows)
|
||||
import CRT
|
||||
import WinSDK
|
||||
#elseif os(WASI)
|
||||
import WASILibc
|
||||
#endif
|
||||
private enum Base64Error: Error {
|
||||
case invalidElementCount
|
||||
case cannotDecode
|
||||
}
|
||||
|
||||
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
|
||||
extension Data {
|
||||
@ -36,10 +26,21 @@ extension Data {
|
||||
/// - parameter base64String: The string to parse.
|
||||
/// - parameter options: Encoding options. Default value is `[]`.
|
||||
public init?(base64Encoded base64String: __shared String, options: Base64DecodingOptions = []) {
|
||||
guard let result = try? Base64.decode(string: base64String, options: options) else {
|
||||
#if FOUNDATION_FRAMEWORK
|
||||
if let d = NSData(base64Encoded: base64String, options: NSData.Base64DecodingOptions(rawValue: options.rawValue)) {
|
||||
self.init(referencing: d)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
self = result
|
||||
#else
|
||||
var encoded = base64String
|
||||
let decoded = encoded.withUTF8 {
|
||||
// String won't pass an empty buffer with a `nil` `baseAddress`.
|
||||
Data(decodingBase64: BufferView(unsafeBufferPointer: $0)!, options: options)
|
||||
}
|
||||
guard let decoded else { return nil }
|
||||
self = decoded
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Initialize a `Data` from a Base-64, UTF-8 encoded `Data`.
|
||||
@ -49,10 +50,39 @@ extension Data {
|
||||
/// - parameter base64Data: Base-64, UTF-8 encoded input data.
|
||||
/// - parameter options: Decoding options. Default value is `[]`.
|
||||
public init?(base64Encoded base64Data: __shared Data, options: Base64DecodingOptions = []) {
|
||||
guard let result = try? Base64.decode(data: base64Data, options: options) else {
|
||||
#if FOUNDATION_FRAMEWORK
|
||||
if let d = NSData(base64Encoded: base64Data, options: NSData.Base64DecodingOptions(rawValue: options.rawValue)) {
|
||||
self.init(referencing: d)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
self = result
|
||||
#else
|
||||
let decoded = base64Data.withBufferView {
|
||||
Data(decodingBase64: $0, options: options)
|
||||
}
|
||||
guard let decoded else { return nil }
|
||||
self = decoded
|
||||
#endif
|
||||
}
|
||||
|
||||
init?(decodingBase64 bytes: borrowing BufferView<UInt8>, options: Base64DecodingOptions = []) {
|
||||
guard bytes.count.isMultiple(of: 4) || options.contains(.ignoreUnknownCharacters)
|
||||
else { return nil }
|
||||
|
||||
// Every 4 valid ASCII bytes maps to 3 output bytes: (bytes.count * 3)/4
|
||||
let capacity = (bytes.count * 3) >> 2
|
||||
// A non-trapping version of the calculation goes like this:
|
||||
// let (q, r) = bytes.count.quotientAndRemainder(dividingBy: 4)
|
||||
// let capacity = (q * 3) + (r==0 ? 0 : r-1)
|
||||
let decoded = try? Data(
|
||||
capacity: capacity,
|
||||
initializingWith: { //FIXME: should work with borrowed `bytes`
|
||||
[bytes = copy bytes] in
|
||||
try Data.base64DecodeBytes(bytes, &$0, options: options)
|
||||
}
|
||||
)
|
||||
guard let decoded else { return nil }
|
||||
self = decoded
|
||||
}
|
||||
|
||||
// MARK: - Create base64
|
||||
@ -72,6 +102,110 @@ extension Data {
|
||||
public func base64EncodedData(options: Base64EncodingOptions = []) -> Data {
|
||||
Base64.encodeToData(bytes: self, options: options)
|
||||
}
|
||||
|
||||
// MARK: - Internal Helpers
|
||||
|
||||
/**
|
||||
This method decodes Base64-encoded data.
|
||||
|
||||
If the input contains any bytes that are not valid Base64 characters,
|
||||
this will throw a `Base64Error`.
|
||||
|
||||
- parameter bytes: The Base64 bytes
|
||||
- parameter output: An OutputBuffer to be filled with decoded bytes
|
||||
- parameter options: Options for handling invalid input
|
||||
- throws: When decoding fails
|
||||
*/
|
||||
static func base64DecodeBytes(
|
||||
_ bytes: borrowing BufferView<UInt8>, _ output: inout OutputBuffer<UInt8>, options: Base64DecodingOptions = []
|
||||
) throws {
|
||||
guard bytes.count.isMultiple(of: 4) || options.contains(.ignoreUnknownCharacters)
|
||||
else { throw Base64Error.invalidElementCount }
|
||||
|
||||
// This table maps byte values 0-127, input bytes >127 are always invalid.
|
||||
// Map the ASCII characters "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -> 0...63
|
||||
// Map '=' (ASCII 61) to 0x40.
|
||||
// All other values map to 0x7f. This allows '=' and invalid bytes to be checked together by testing bit 6 (0x40).
|
||||
let base64Decode: StaticString = """
|
||||
\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\
|
||||
\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\
|
||||
\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{3e}\u{7f}\u{7f}\u{7f}\u{3f}\
|
||||
\u{34}\u{35}\u{36}\u{37}\u{38}\u{39}\u{3a}\u{3b}\u{3c}\u{3d}\u{7f}\u{7f}\u{7f}\u{40}\u{7f}\u{7f}\
|
||||
\u{7f}\u{00}\u{01}\u{02}\u{03}\u{04}\u{05}\u{06}\u{07}\u{08}\u{09}\u{0a}\u{0b}\u{0c}\u{0d}\u{0e}\
|
||||
\u{0f}\u{10}\u{11}\u{12}\u{13}\u{14}\u{15}\u{16}\u{17}\u{18}\u{19}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\
|
||||
\u{7f}\u{1a}\u{1b}\u{1c}\u{1d}\u{1e}\u{1f}\u{20}\u{21}\u{22}\u{23}\u{24}\u{25}\u{26}\u{27}\u{28}\
|
||||
\u{29}\u{2a}\u{2b}\u{2c}\u{2d}\u{2e}\u{2f}\u{30}\u{31}\u{32}\u{33}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}
|
||||
"""
|
||||
assert(base64Decode.isASCII)
|
||||
assert(base64Decode.utf8CodeUnitCount == 128)
|
||||
assert(base64Decode.hasPointerRepresentation)
|
||||
|
||||
let ignoreUnknown = options.contains(.ignoreUnknownCharacters)
|
||||
|
||||
var currentByte: UInt8 = 0
|
||||
var validCharacterCount = 0
|
||||
var paddingCount = 0
|
||||
var index = 0
|
||||
|
||||
for base64Char in bytes {
|
||||
var value: UInt8 = 0
|
||||
|
||||
var invalid = false
|
||||
if base64Char >= base64Decode.utf8CodeUnitCount {
|
||||
invalid = true
|
||||
} else {
|
||||
value = base64Decode.utf8Start[Int(base64Char)]
|
||||
if value & 0x40 == 0x40 { // Input byte is either '=' or an invalid value.
|
||||
if value == 0x7f {
|
||||
invalid = true
|
||||
} else if value == 0x40 { // '=' padding at end of input.
|
||||
paddingCount += 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if invalid {
|
||||
if ignoreUnknown {
|
||||
continue
|
||||
} else {
|
||||
throw Base64Error.cannotDecode
|
||||
}
|
||||
}
|
||||
validCharacterCount += 1
|
||||
|
||||
// Padding found in the middle of the sequence is invalid.
|
||||
if paddingCount > 0 {
|
||||
throw Base64Error.cannotDecode
|
||||
}
|
||||
|
||||
switch index {
|
||||
case 0:
|
||||
currentByte = (value << 2)
|
||||
case 1:
|
||||
currentByte |= (value >> 4)
|
||||
output.appendElement(currentByte)
|
||||
currentByte = (value << 4)
|
||||
case 2:
|
||||
currentByte |= (value >> 2)
|
||||
output.appendElement(currentByte)
|
||||
currentByte = (value << 6)
|
||||
case 3:
|
||||
currentByte |= value
|
||||
output.appendElement(currentByte)
|
||||
index = -1
|
||||
default:
|
||||
fatalError("Invalid state")
|
||||
}
|
||||
|
||||
index += 1
|
||||
}
|
||||
|
||||
guard (validCharacterCount + paddingCount) % 4 == 0 else {
|
||||
// Invalid character count of valid input characters.
|
||||
throw Base64Error.cannotDecode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This base64 implementation is heavily inspired by:
|
||||
@ -212,7 +346,7 @@ extension Base64 {
|
||||
return self._encodeWithLineBreaks(input: input, buffer: buffer, length: &length, options: options)
|
||||
}
|
||||
|
||||
let omitPaddingCharacter = false
|
||||
let omitPaddingCharacter = false // options.contains(.omitPaddingCharacter)
|
||||
|
||||
Self.withUnsafeEncodingTablesAsBufferPointers(options: options) { (e0, e1) throws(Never) -> Void in
|
||||
let to = input.count / 3 * 3
|
||||
@ -413,613 +547,3 @@ extension Base64 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Decoding -
|
||||
|
||||
extension Base64 {
|
||||
|
||||
enum DecodingError: Error, Equatable {
|
||||
case invalidLength
|
||||
case invalidCharacter(UInt8)
|
||||
case unexpectedPaddingCharacter
|
||||
case unexpectedEnd
|
||||
}
|
||||
|
||||
static func decode(string encoded: String, options: Data.Base64DecodingOptions = []) throws(DecodingError) -> Data {
|
||||
let result = encoded.utf8.withContiguousStorageIfAvailable { bufferPointer in
|
||||
// `withContiguousStorageIfAvailable` sadly does not support typed throws, so we need
|
||||
// to use Result to get the error out without allocation for the error.
|
||||
Result(catching: { () throws(DecodingError) -> Data in
|
||||
try Self._decodeToData(from: bufferPointer, options: options)
|
||||
})
|
||||
}
|
||||
|
||||
if let result {
|
||||
return try result.get()
|
||||
}
|
||||
|
||||
var encoded = encoded
|
||||
encoded.makeContiguousUTF8()
|
||||
return try Self.decode(string: encoded, options: options)
|
||||
}
|
||||
|
||||
static func decode(data encoded: Data, options: Data.Base64DecodingOptions = []) throws(DecodingError) -> Data? {
|
||||
let result = encoded.withContiguousStorageIfAvailable { bufferPointer in
|
||||
// `withContiguousStorageIfAvailable` sadly does not support typed throws, so we need
|
||||
// to use Result to get the error out without allocation for the error.
|
||||
Result(catching: { () throws(DecodingError) -> Data in
|
||||
try Self._decodeToData(from: bufferPointer, options: options)
|
||||
})
|
||||
}
|
||||
|
||||
if let result {
|
||||
return try result.get()
|
||||
}
|
||||
|
||||
return try Self.decode(bytes: Array(encoded), options: options)
|
||||
}
|
||||
|
||||
static func decode<Buffer: Collection>(bytes: Buffer, options: Data.Base64DecodingOptions = []) throws(DecodingError) -> Data where Buffer.Element == UInt8 {
|
||||
guard bytes.count > 0 else {
|
||||
return Data()
|
||||
}
|
||||
|
||||
let result = bytes.withContiguousStorageIfAvailable { bufferPointer in
|
||||
// `withContiguousStorageIfAvailable` sadly does not support typed throws, so we need
|
||||
// to use Result to get the error out without allocation for the error.
|
||||
Result(catching: { () throws(DecodingError) -> Data in
|
||||
try Self._decodeToData(from: bufferPointer, options: options)
|
||||
})
|
||||
}
|
||||
|
||||
if let result {
|
||||
return try result.get()
|
||||
}
|
||||
|
||||
return try self.decode(bytes: Array(bytes), options: options)
|
||||
}
|
||||
|
||||
static func _decodeToData(from inBuffer: UnsafeBufferPointer<UInt8>, options: Data.Base64DecodingOptions) throws(DecodingError) -> Data {
|
||||
guard inBuffer.count > 0 else {
|
||||
return Data()
|
||||
}
|
||||
|
||||
let outputLength = ((inBuffer.count + 3) / 4) * 3
|
||||
|
||||
let pointer = malloc(outputLength)
|
||||
let other = pointer?.bindMemory(to: UInt8.self, capacity: outputLength)
|
||||
let target = UnsafeMutableBufferPointer(start: other, count: outputLength)
|
||||
var length = outputLength
|
||||
if options.contains(.ignoreUnknownCharacters) {
|
||||
try Self._decodeIgnoringErrors(from: inBuffer, into: target, length: &length, options: options)
|
||||
} else {
|
||||
// for whatever reason I can see this being 10% faster for larger payloads. Maybe better
|
||||
// branch prediction?
|
||||
try self._decode(from: inBuffer, into: target, length: &length, options: options)
|
||||
}
|
||||
|
||||
return Data(bytesNoCopy: pointer!, count: length, deallocator: .free)
|
||||
}
|
||||
|
||||
static func _decode(
|
||||
from inBuffer: UnsafeBufferPointer<UInt8>,
|
||||
into outBuffer: UnsafeMutableBufferPointer<UInt8>,
|
||||
length: inout Int,
|
||||
options: Data.Base64DecodingOptions
|
||||
) throws(DecodingError) {
|
||||
let remaining = inBuffer.count % 4
|
||||
guard remaining == 0 else { throw DecodingError.invalidLength }
|
||||
|
||||
let outputLength = ((inBuffer.count + 3) / 4) * 3
|
||||
let fullchunks = remaining == 0 ? inBuffer.count / 4 - 1 : inBuffer.count / 4
|
||||
guard outBuffer.count >= outputLength else {
|
||||
preconditionFailure("Expected the out buffer to be at least as long as outputLength")
|
||||
}
|
||||
|
||||
try Self.withUnsafeDecodingTablesAsBufferPointers(options: options) { (d0, d1, d2, d3) throws(DecodingError) in
|
||||
var outIndex = 0
|
||||
if fullchunks > 0 {
|
||||
for chunk in 0 ..< fullchunks {
|
||||
let inIndex = chunk * 4
|
||||
let a0 = inBuffer[inIndex]
|
||||
let a1 = inBuffer[inIndex + 1]
|
||||
let a2 = inBuffer[inIndex + 2]
|
||||
let a3 = inBuffer[inIndex + 3]
|
||||
var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2)] | d3[Int(a3)]
|
||||
|
||||
if x >= Self.badCharacter {
|
||||
// TODO: Inspect characters here better
|
||||
throw DecodingError.invalidCharacter(inBuffer[inIndex])
|
||||
}
|
||||
|
||||
withUnsafePointer(to: &x) { ptr in
|
||||
ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in
|
||||
outBuffer[outIndex] = newPtr[0]
|
||||
outBuffer[outIndex + 1] = newPtr[1]
|
||||
outBuffer[outIndex + 2] = newPtr[2]
|
||||
outIndex += 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inIndex is the first index in the last chunk
|
||||
let inIndex = fullchunks * 4
|
||||
let a0 = inBuffer[inIndex]
|
||||
let a1 = inBuffer[inIndex + 1]
|
||||
var a2: UInt8?
|
||||
var a3: UInt8?
|
||||
if inIndex + 2 < inBuffer.count, inBuffer[inIndex + 2] != Self.encodePaddingCharacter {
|
||||
a2 = inBuffer[inIndex + 2]
|
||||
}
|
||||
if inIndex + 3 < inBuffer.count, inBuffer[inIndex + 3] != Self.encodePaddingCharacter {
|
||||
a3 = inBuffer[inIndex + 3]
|
||||
}
|
||||
|
||||
var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2 ?? 65)] | d3[Int(a3 ?? 65)]
|
||||
if x >= Self.badCharacter {
|
||||
// TODO: Inspect characters here better
|
||||
throw DecodingError.invalidCharacter(inBuffer[inIndex])
|
||||
}
|
||||
|
||||
withUnsafePointer(to: &x) { ptr in
|
||||
ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in
|
||||
outBuffer[outIndex] = newPtr[0]
|
||||
outIndex += 1
|
||||
if a2 != nil {
|
||||
outBuffer[outIndex] = newPtr[1]
|
||||
outIndex += 1
|
||||
}
|
||||
if a3 != nil {
|
||||
outBuffer[outIndex] = newPtr[2]
|
||||
outIndex += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
length = outIndex
|
||||
}
|
||||
}
|
||||
|
||||
static func _decodeIgnoringErrors(
|
||||
from inBuffer: UnsafeBufferPointer<UInt8>,
|
||||
into outBuffer: UnsafeMutableBufferPointer<UInt8>,
|
||||
length: inout Int,
|
||||
options: Data.Base64DecodingOptions
|
||||
) throws(DecodingError) {
|
||||
assert(options.contains(.ignoreUnknownCharacters))
|
||||
|
||||
let outputLength = ((inBuffer.count + 3) / 4) * 3
|
||||
guard outBuffer.count >= outputLength else {
|
||||
preconditionFailure("Expected the out buffer to be at least as long as outputLength")
|
||||
}
|
||||
|
||||
try Self.withUnsafeDecodingTablesAsBufferPointers(options: options) { (d0, d1, d2, d3) throws(DecodingError) in
|
||||
var outIndex = 0
|
||||
var inIndex = 0
|
||||
|
||||
fastLoop: while inIndex + 3 < inBuffer.count {
|
||||
let a0 = inBuffer[inIndex]
|
||||
let a1 = inBuffer[inIndex &+ 1]
|
||||
let a2 = inBuffer[inIndex &+ 2]
|
||||
let a3 = inBuffer[inIndex &+ 3]
|
||||
var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2)] | d3[Int(a3)]
|
||||
|
||||
if x >= Self.badCharacter {
|
||||
if a3 == Self.encodePaddingCharacter {
|
||||
break // the loop
|
||||
}
|
||||
|
||||
// error fast path. we assume that illeagal errors are at the boundary.
|
||||
// lets skip them and then return to fast mode!
|
||||
if !self.isValidBase64Byte(a0, options: options) {
|
||||
if !self.isValidBase64Byte(a1, options: options) {
|
||||
if !self.isValidBase64Byte(a2, options: options) {
|
||||
if !self.isValidBase64Byte(a3, options: options) {
|
||||
inIndex &+= 4
|
||||
continue
|
||||
} else {
|
||||
inIndex &+= 3
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
inIndex &+= 2
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
inIndex &+= 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// error slow path... the first character is valid base64
|
||||
let b0 = a0
|
||||
var b1: UInt8? = nil
|
||||
var b2: UInt8? = nil
|
||||
var b3: UInt8? = nil
|
||||
let startIndex = inIndex
|
||||
inIndex &+= 1
|
||||
scanForValidCharacters: while inIndex < inBuffer.count {
|
||||
guard self.isValidBase64Byte(inBuffer[inIndex], options: options) else {
|
||||
if inBuffer[inIndex] == Self.encodePaddingCharacter {
|
||||
inIndex = startIndex
|
||||
break fastLoop
|
||||
}
|
||||
inIndex &+= 1
|
||||
continue scanForValidCharacters
|
||||
}
|
||||
|
||||
defer { inIndex &+= 1 }
|
||||
|
||||
if b1 == nil {
|
||||
b1 = inBuffer[inIndex]
|
||||
} else if b2 == nil {
|
||||
b2 = inBuffer[inIndex]
|
||||
} else if b3 == nil {
|
||||
b3 = inBuffer[inIndex]
|
||||
break scanForValidCharacters
|
||||
}
|
||||
}
|
||||
|
||||
guard let b1, let b2, let b3 else {
|
||||
throw DecodingError.invalidLength
|
||||
}
|
||||
|
||||
x = d0[Int(b0)] | d1[Int(b1)] | d2[Int(b2)] | d3[Int(b3)]
|
||||
|
||||
} else {
|
||||
inIndex &+= 4
|
||||
}
|
||||
|
||||
withUnsafePointer(to: &x) { ptr in
|
||||
ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in
|
||||
outBuffer[outIndex] = newPtr[0]
|
||||
outBuffer[outIndex &+ 1] = newPtr[1]
|
||||
outBuffer[outIndex &+ 2] = newPtr[2]
|
||||
outIndex &+= 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inIndex == inBuffer.count {
|
||||
// all done!
|
||||
length = outIndex
|
||||
return
|
||||
}
|
||||
|
||||
guard inIndex + 3 < inBuffer.count else {
|
||||
if options.contains(.ignoreUnknownCharacters) {
|
||||
// ensure that all remaining characters are unknown
|
||||
while inIndex < inBuffer.count {
|
||||
let value = inBuffer[inIndex]
|
||||
if self.isValidBase64Byte(value, options: options) || value == Self.encodePaddingCharacter {
|
||||
throw DecodingError.invalidCharacter(inBuffer[inIndex])
|
||||
}
|
||||
inIndex &+= 1
|
||||
}
|
||||
length = outIndex
|
||||
return
|
||||
}
|
||||
throw DecodingError.invalidLength
|
||||
}
|
||||
|
||||
let a0 = inBuffer[inIndex]
|
||||
let a1 = inBuffer[inIndex + 1]
|
||||
var a2: UInt8 = 65
|
||||
var a3: UInt8 = 65
|
||||
var padding2 = false
|
||||
var padding3 = false
|
||||
|
||||
if inBuffer[inIndex + 2] == Self.encodePaddingCharacter {
|
||||
padding2 = true
|
||||
} else {
|
||||
a2 = inBuffer[inIndex + 2]
|
||||
}
|
||||
if inBuffer[inIndex + 3] == Self.encodePaddingCharacter {
|
||||
padding3 = true
|
||||
} else {
|
||||
if padding2 && self.isValidBase64Byte(inBuffer[inIndex + 3], options: options) {
|
||||
throw DecodingError.unexpectedPaddingCharacter
|
||||
}
|
||||
a3 = inBuffer[inIndex + 3]
|
||||
}
|
||||
|
||||
var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2)] | d3[Int(a3)]
|
||||
if x >= Self.badCharacter {
|
||||
var b0: UInt8? = nil
|
||||
var b1: UInt8? = nil
|
||||
var b2: UInt8? = nil
|
||||
var b3: UInt8? = nil
|
||||
|
||||
scanForValidCharacters: while inIndex < inBuffer.count {
|
||||
defer { inIndex &+= 1 }
|
||||
let value = inBuffer[inIndex]
|
||||
if self.isValidBase64Byte(value, options: options) {
|
||||
if b0 == nil {
|
||||
b0 = value
|
||||
} else if b1 == nil {
|
||||
b1 = value
|
||||
} else if b2 == nil {
|
||||
b2 = value
|
||||
} else if b3 == nil {
|
||||
if padding2 { throw DecodingError.unexpectedPaddingCharacter }
|
||||
b3 = value
|
||||
break scanForValidCharacters
|
||||
}
|
||||
} else if value == Self.encodePaddingCharacter {
|
||||
guard b0 != nil, b1 != nil else {
|
||||
throw DecodingError.invalidLength
|
||||
}
|
||||
if b2 == nil {
|
||||
padding2 = true
|
||||
b2 = 65
|
||||
} else if b3 == nil {
|
||||
padding3 = true
|
||||
b3 = 65
|
||||
break scanForValidCharacters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let b0, let b1, let b2, let b3 else {
|
||||
if b0 == nil {
|
||||
length = outIndex
|
||||
return
|
||||
}
|
||||
throw DecodingError.invalidLength
|
||||
}
|
||||
|
||||
x = d0[Int(b0)] | d1[Int(b1)] | d2[Int(b2)] | d3[Int(b3)]
|
||||
assert(x < Self.badCharacter)
|
||||
}
|
||||
|
||||
withUnsafePointer(to: &x) { ptr in
|
||||
ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in
|
||||
outBuffer[outIndex] = newPtr[0]
|
||||
outIndex += 1
|
||||
if !padding2 {
|
||||
outBuffer[outIndex] = newPtr[1]
|
||||
outIndex += 1
|
||||
}
|
||||
if !padding3 {
|
||||
outBuffer[outIndex] = newPtr[2]
|
||||
outIndex += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
length = outIndex
|
||||
}
|
||||
}
|
||||
|
||||
static func withUnsafeDecodingTablesAsBufferPointers<R, E: Swift.Error>(options: Data.Base64DecodingOptions, _ body: (UnsafeBufferPointer<UInt32>, UnsafeBufferPointer<UInt32>, UnsafeBufferPointer<UInt32>, UnsafeBufferPointer<UInt32>) throws(E) -> R) throws(E) -> R {
|
||||
let decoding0 = Self.decoding0
|
||||
let decoding1 = Self.decoding1
|
||||
let decoding2 = Self.decoding2
|
||||
let decoding3 = Self.decoding3
|
||||
|
||||
assert(decoding0.count == 256)
|
||||
assert(decoding1.count == 256)
|
||||
assert(decoding2.count == 256)
|
||||
assert(decoding3.count == 256)
|
||||
|
||||
// Workaround that `withUnsafeBufferPointer` started to support typed throws in Swift 6.1
|
||||
let result = decoding0.withUnsafeBufferPointer { d0 -> Result<R, E> in
|
||||
decoding1.withUnsafeBufferPointer { d1 -> Result<R, E> in
|
||||
decoding2.withUnsafeBufferPointer { d2 -> Result<R, E> in
|
||||
decoding3.withUnsafeBufferPointer { d3 -> Result<R, E> in
|
||||
Result { () throws(E) -> R in
|
||||
try body(d0, d1, d2, d3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return try result.get()
|
||||
}
|
||||
|
||||
static func isValidBase64Byte(_ byte: UInt8, options: Data.Base64DecodingOptions) -> Bool {
|
||||
switch byte {
|
||||
case UInt8(ascii: "A")...UInt8(ascii: "Z"),
|
||||
UInt8(ascii: "a")...UInt8(ascii: "z"),
|
||||
UInt8(ascii: "0")...UInt8(ascii: "9"):
|
||||
true
|
||||
|
||||
case UInt8(ascii: "-"), UInt8(ascii: "_"):
|
||||
false // options.contains(.base64UrlAlphabet)
|
||||
|
||||
case UInt8(ascii: "/"), UInt8(ascii: "+"):
|
||||
true // !options.contains(.base64UrlAlphabet)
|
||||
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
static let badCharacter: UInt32 = 0x01FF_FFFF
|
||||
|
||||
static let decoding0: [UInt32] = [
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x0000_00F8, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_00FC,
|
||||
0x0000_00D0, 0x0000_00D4, 0x0000_00D8, 0x0000_00DC, 0x0000_00E0, 0x0000_00E4,
|
||||
0x0000_00E8, 0x0000_00EC, 0x0000_00F0, 0x0000_00F4, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000,
|
||||
0x0000_0004, 0x0000_0008, 0x0000_000C, 0x0000_0010, 0x0000_0014, 0x0000_0018,
|
||||
0x0000_001C, 0x0000_0020, 0x0000_0024, 0x0000_0028, 0x0000_002C, 0x0000_0030,
|
||||
0x0000_0034, 0x0000_0038, 0x0000_003C, 0x0000_0040, 0x0000_0044, 0x0000_0048,
|
||||
0x0000_004C, 0x0000_0050, 0x0000_0054, 0x0000_0058, 0x0000_005C, 0x0000_0060,
|
||||
0x0000_0064, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x0000_0068, 0x0000_006C, 0x0000_0070, 0x0000_0074, 0x0000_0078,
|
||||
0x0000_007C, 0x0000_0080, 0x0000_0084, 0x0000_0088, 0x0000_008C, 0x0000_0090,
|
||||
0x0000_0094, 0x0000_0098, 0x0000_009C, 0x0000_00A0, 0x0000_00A4, 0x0000_00A8,
|
||||
0x0000_00AC, 0x0000_00B0, 0x0000_00B4, 0x0000_00B8, 0x0000_00BC, 0x0000_00C0,
|
||||
0x0000_00C4, 0x0000_00C8, 0x0000_00CC, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
]
|
||||
|
||||
static let decoding1: [UInt32] = [
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x0000_E003, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_F003,
|
||||
0x0000_4003, 0x0000_5003, 0x0000_6003, 0x0000_7003, 0x0000_8003, 0x0000_9003,
|
||||
0x0000_A003, 0x0000_B003, 0x0000_C003, 0x0000_D003, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000,
|
||||
0x0000_1000, 0x0000_2000, 0x0000_3000, 0x0000_4000, 0x0000_5000, 0x0000_6000,
|
||||
0x0000_7000, 0x0000_8000, 0x0000_9000, 0x0000_A000, 0x0000_B000, 0x0000_C000,
|
||||
0x0000_D000, 0x0000_E000, 0x0000_F000, 0x0000_0001, 0x0000_1001, 0x0000_2001,
|
||||
0x0000_3001, 0x0000_4001, 0x0000_5001, 0x0000_6001, 0x0000_7001, 0x0000_8001,
|
||||
0x0000_9001, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x0000_A001, 0x0000_B001, 0x0000_C001, 0x0000_D001, 0x0000_E001,
|
||||
0x0000_F001, 0x0000_0002, 0x0000_1002, 0x0000_2002, 0x0000_3002, 0x0000_4002,
|
||||
0x0000_5002, 0x0000_6002, 0x0000_7002, 0x0000_8002, 0x0000_9002, 0x0000_A002,
|
||||
0x0000_B002, 0x0000_C002, 0x0000_D002, 0x0000_E002, 0x0000_F002, 0x0000_0003,
|
||||
0x0000_1003, 0x0000_2003, 0x0000_3003, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
]
|
||||
|
||||
static let decoding2: [UInt32] = [
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x0080_0F00, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x00C0_0F00,
|
||||
0x0000_0D00, 0x0040_0D00, 0x0080_0D00, 0x00C0_0D00, 0x0000_0E00, 0x0040_0E00,
|
||||
0x0080_0E00, 0x00C0_0E00, 0x0000_0F00, 0x0040_0F00, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000,
|
||||
0x0040_0000, 0x0080_0000, 0x00C0_0000, 0x0000_0100, 0x0040_0100, 0x0080_0100,
|
||||
0x00C0_0100, 0x0000_0200, 0x0040_0200, 0x0080_0200, 0x00C0_0200, 0x0000_0300,
|
||||
0x0040_0300, 0x0080_0300, 0x00C0_0300, 0x0000_0400, 0x0040_0400, 0x0080_0400,
|
||||
0x00C0_0400, 0x0000_0500, 0x0040_0500, 0x0080_0500, 0x00C0_0500, 0x0000_0600,
|
||||
0x0040_0600, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x0080_0600, 0x00C0_0600, 0x0000_0700, 0x0040_0700, 0x0080_0700,
|
||||
0x00C0_0700, 0x0000_0800, 0x0040_0800, 0x0080_0800, 0x00C0_0800, 0x0000_0900,
|
||||
0x0040_0900, 0x0080_0900, 0x00C0_0900, 0x0000_0A00, 0x0040_0A00, 0x0080_0A00,
|
||||
0x00C0_0A00, 0x0000_0B00, 0x0040_0B00, 0x0080_0B00, 0x00C0_0B00, 0x0000_0C00,
|
||||
0x0040_0C00, 0x0080_0C00, 0x00C0_0C00, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
]
|
||||
|
||||
static let decoding3: [UInt32] = [
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x003E_0000, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x003F_0000,
|
||||
0x0034_0000, 0x0035_0000, 0x0036_0000, 0x0037_0000, 0x0038_0000, 0x0039_0000,
|
||||
0x003A_0000, 0x003B_0000, 0x003C_0000, 0x003D_0000, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000,
|
||||
0x0001_0000, 0x0002_0000, 0x0003_0000, 0x0004_0000, 0x0005_0000, 0x0006_0000,
|
||||
0x0007_0000, 0x0008_0000, 0x0009_0000, 0x000A_0000, 0x000B_0000, 0x000C_0000,
|
||||
0x000D_0000, 0x000E_0000, 0x000F_0000, 0x0010_0000, 0x0011_0000, 0x0012_0000,
|
||||
0x0013_0000, 0x0014_0000, 0x0015_0000, 0x0016_0000, 0x0017_0000, 0x0018_0000,
|
||||
0x0019_0000, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x001A_0000, 0x001B_0000, 0x001C_0000, 0x001D_0000, 0x001E_0000,
|
||||
0x001F_0000, 0x0020_0000, 0x0021_0000, 0x0022_0000, 0x0023_0000, 0x0024_0000,
|
||||
0x0025_0000, 0x0026_0000, 0x0027_0000, 0x0028_0000, 0x0029_0000, 0x002A_0000,
|
||||
0x002B_0000, 0x002C_0000, 0x002D_0000, 0x002E_0000, 0x002F_0000, 0x0030_0000,
|
||||
0x0031_0000, 0x0032_0000, 0x0033_0000, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF,
|
||||
]
|
||||
}
|
||||
|
@ -31,6 +31,10 @@ import WinSDK
|
||||
@preconcurrency import WASILibc
|
||||
#endif
|
||||
|
||||
#if FOUNDATION_FRAMEWORK && NO_FILESYSTEM
|
||||
@_spi(ENABLE_EXCLAVE_STORAGE) import C
|
||||
#endif
|
||||
|
||||
func _fgetxattr(_ fd: Int32, _ name: UnsafePointer<CChar>!, _ value: UnsafeMutableRawPointer!, _ size: Int, _ position: UInt32, _ options: Int32) -> Int {
|
||||
#if canImport(Darwin)
|
||||
return fgetxattr(fd, name, value, size, position, options)
|
||||
@ -329,13 +333,13 @@ internal func readBytesFromFile(path inPath: PathOrURL, reportProgress: Bool, ma
|
||||
}
|
||||
|
||||
let fileSize = min(Int(clamping: filestat.st_size), maxLength ?? Int.max)
|
||||
let fileType = mode_t(filestat.st_mode) & S_IFMT
|
||||
let fileType = mode_t(filestat.st_mode) & mode_t(S_IFMT)
|
||||
#if !NO_FILESYSTEM
|
||||
let shouldMap = shouldMapFileDescriptor(fd, path: inPath, options: options)
|
||||
#else
|
||||
let shouldMap = false
|
||||
#endif
|
||||
|
||||
|
||||
if fileType != S_IFREG {
|
||||
// EACCES is still an odd choice, but at least we have a better error for directories.
|
||||
let code = (fileType == S_IFDIR) ? EISDIR : EACCES
|
||||
|
@ -12,8 +12,10 @@
|
||||
|
||||
#if FOUNDATION_FRAMEWORK
|
||||
internal import _ForSwiftFoundation
|
||||
#if !NO_FILESYSTEM
|
||||
internal import DarwinPrivate // for VREG
|
||||
#endif
|
||||
#endif
|
||||
|
||||
internal import _FoundationCShims
|
||||
|
||||
|
@ -2135,10 +2135,6 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
|
||||
/// - parameter options: Options for the read operation. Default value is `[]`.
|
||||
/// - throws: An error in the Cocoa domain, if `url` cannot be read.
|
||||
public init(contentsOf url: __shared URL, options: ReadingOptions = []) throws {
|
||||
#if NO_FILESYSTEM
|
||||
let d = try NSData(contentsOf: url, options: NSData.ReadingOptions(rawValue: options.rawValue))
|
||||
self.init(referencing: d)
|
||||
#else
|
||||
if url.isFileURL {
|
||||
self = try readDataFromFile(path: .url(url), reportProgress: true, options: options)
|
||||
} else {
|
||||
@ -2150,16 +2146,10 @@ public struct Data : Equatable, Hashable, RandomAccessCollection, MutableCollect
|
||||
try self.init(_contentsOfRemote: url, options: options)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
internal init(contentsOfFile path: String, options: ReadingOptions = []) throws {
|
||||
#if NO_FILESYSTEM
|
||||
let d = try NSData(contentsOfFile: path, options: NSData.ReadingOptions(rawValue: options.rawValue))
|
||||
self.init(referencing: d)
|
||||
#else
|
||||
self = try readDataFromFile(path: .path(path), reportProgress: true, options: options)
|
||||
#endif
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
|
@ -201,7 +201,40 @@ extension stat {
|
||||
}
|
||||
}
|
||||
|
||||
#if FOUNDATION_FRAMEWORK
|
||||
extension FileProtectionType {
|
||||
var intValue: Int32? {
|
||||
switch self {
|
||||
case .complete: PROTECTION_CLASS_A
|
||||
case .init(rawValue: "NSFileProtectionWriteOnly"), .completeUnlessOpen: PROTECTION_CLASS_B
|
||||
case .init(rawValue: "NSFileProtectionCompleteUntilUserAuthentication"), .completeUntilFirstUserAuthentication: PROTECTION_CLASS_C
|
||||
case .none: PROTECTION_CLASS_D
|
||||
#if !os(macOS)
|
||||
case .completeWhenUserInactive: PROTECTION_CLASS_CX
|
||||
#endif
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
init?(intValue value: Int32) {
|
||||
switch value {
|
||||
case PROTECTION_CLASS_A: self = .complete
|
||||
case PROTECTION_CLASS_B: self = .completeUnlessOpen
|
||||
case PROTECTION_CLASS_C: self = .completeUntilFirstUserAuthentication
|
||||
case PROTECTION_CLASS_D: self = .none
|
||||
#if !os(macOS)
|
||||
case PROTECTION_CLASS_CX: self = .completeWhenUserInactive
|
||||
#endif
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extension FileAttributeKey {
|
||||
fileprivate static var _extendedAttributes: Self { Self("NSFileExtendedAttributes") }
|
||||
}
|
||||
|
||||
extension _FileManagerImpl {
|
||||
func createFile(
|
||||
|
@ -69,6 +69,12 @@ extension stat {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FOUNDATION_FRAMEWORK && os(macOS)
|
||||
extension URLResourceKey {
|
||||
static var _finderInfoKey: Self { URLResourceKey("_NSURLFinderInfoKey") }
|
||||
}
|
||||
#endif
|
||||
|
||||
extension _FileManagerImpl {
|
||||
#if os(macOS) && FOUNDATION_FRAMEWORK
|
||||
private struct _HFSFinderInfo {
|
||||
|
@ -960,6 +960,49 @@ enum _FileOperations {
|
||||
private static func _copyDirectoryMetadata(srcFD: CInt, srcPath: @autoclosure () -> String, dstFD: CInt, dstPath: @autoclosure () -> String, delegate: some LinkOrCopyDelegate) throws {
|
||||
#if !os(WASI) && !os(Android)
|
||||
// Copy extended attributes
|
||||
#if os(FreeBSD)
|
||||
// FreeBSD uses the `extattr_*` calls for setting extended attributes. Unlike like, the namespace for the extattrs are not determined by prefix of the attribute
|
||||
for namespace in [EXTATTR_NAMESPACE_SYSTEM, EXTATTR_NAMESPACE_USER] {
|
||||
// if we don't have permission to list attributes in system namespace, this returns -1 and skips it
|
||||
var size = extattr_list_fd(srcFD, namespace, nil, 0)
|
||||
if size > 0 {
|
||||
// we are allocating size + 1 bytes here such that we have room for the last null terminator
|
||||
try withUnsafeTemporaryAllocation(of: CChar.self, capacity: size + 1) { keyList in
|
||||
// The list of entry returns by `extattr_list_*` contains the length(1 byte) of the attribute name, follow by the Non-NULL terminated attribute name. (See exattr(2))
|
||||
size = extattr_list_fd(srcFD, namespace, keyList.baseAddress!, size)
|
||||
|
||||
guard size > 0 else { continue }
|
||||
|
||||
var keyLength = Int(keyList.baseAddress!.pointee)
|
||||
var current = keyList.baseAddress!.advanced(by: 1)
|
||||
let end = keyList.baseAddress!.advanced(by: size)
|
||||
keyList.baseAddress!.advanced(by: size).pointee = 0
|
||||
|
||||
while current < end {
|
||||
let nextEntry = current.advanced(by: keyLength)
|
||||
// get the length of next key, if this is the last entry, this points to the explicitly zerod byte at `end`.
|
||||
keyLength = Int(nextEntry.pointee)
|
||||
// zero the length field of the next name, so current name can pass in as a null-terminated string
|
||||
nextEntry.pointee = 0
|
||||
// this also set `current` to `end` after iterating all entries
|
||||
defer { current = nextEntry.advanced(by: 1) }
|
||||
|
||||
var valueSize = extattr_get_fd(srcFD, namespace, current, nil, 0)
|
||||
if valueSize >= 0 {
|
||||
try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: valueSize) { valueBuffer in
|
||||
valueSize = extattr_get_fd(srcFD, namespace, current, valueBuffer.baseAddress!, valueSize)
|
||||
if valueSize >= 0 {
|
||||
if extattr_set_fd(srcFD, namespace, current, valueBuffer.baseAddress!, valueSize) != 0 {
|
||||
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
var size = flistxattr(srcFD, nil, 0)
|
||||
if size > 0 {
|
||||
try withUnsafeTemporaryAllocation(of: CChar.self, capacity: size) { keyList in
|
||||
@ -985,6 +1028,7 @@ enum _FileOperations {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
var statInfo = stat()
|
||||
if fstat(srcFD, &statInfo) == 0 {
|
||||
#if !os(WASI) // WASI doesn't have fchown for now
|
||||
|
@ -0,0 +1,452 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2025 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(FoundationPreview 6.2, *)
|
||||
public extension FormatStyle where Self == Date.HTTPFormatStyle {
|
||||
static var http: Self {
|
||||
return Date.HTTPFormatStyle()
|
||||
}
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
public extension ParseableFormatStyle where Self == Date.HTTPFormatStyle {
|
||||
static var http: Self { .init() }
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
public extension ParseStrategy where Self == Date.HTTPFormatStyle {
|
||||
@_disfavoredOverload
|
||||
static var http: Self { .init() }
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension Date.HTTPFormatStyle : ParseStrategy {
|
||||
public var parseStrategy: Date.HTTPFormatStyle { self }
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension Date.HTTPFormatStyle : FormatStyle {
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension Date {
|
||||
/// Options for generating and parsing string representations of dates following the HTTP date format from [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110.html#http.date).
|
||||
public struct HTTPFormatStyle : Sendable, Hashable, Codable, ParseableFormatStyle {
|
||||
let componentsStyle = DateComponents.HTTPFormatStyle()
|
||||
|
||||
public init() {}
|
||||
public init(from decoder: any Decoder) throws {}
|
||||
|
||||
public func format(_ date: Date) -> String {
|
||||
// <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
|
||||
let components = Calendar(identifier: .gregorian)._dateComponents([.weekday, .day, .month, .year, .hour, .minute, .second], from: date, in: .gmt)
|
||||
return componentsStyle.format(components)
|
||||
}
|
||||
|
||||
public func parse(_ value: String) throws -> Date {
|
||||
guard let (_, date) = parse(value, in: value.startIndex..<value.endIndex) else {
|
||||
throw parseError(value, exampleFormattedString: self.format(Date.now))
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
fileprivate func parse(_ value: String, in range: Range<String.Index>) -> (String.Index, Date)? {
|
||||
var v = value[range]
|
||||
guard !v.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = v.withUTF8 { buffer -> (Int, Date)? in
|
||||
let view = BufferView(unsafeBufferPointer: buffer)!
|
||||
|
||||
guard let comps = try? componentsStyle.components(from: value, in: view) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HTTP dates are always GMT
|
||||
guard let date = Calendar(identifier: .gregorian).date(from: comps.components) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (comps.consumed, date)
|
||||
}
|
||||
|
||||
guard let result else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let endIndex = value.utf8.index(v.startIndex, offsetBy: result.0)
|
||||
return (endIndex, result.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Regex
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension Date.HTTPFormatStyle : CustomConsumingRegexComponent {
|
||||
public typealias RegexOutput = Date
|
||||
public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: Date)? {
|
||||
guard index < bounds.upperBound else {
|
||||
return nil
|
||||
}
|
||||
// It's important to return nil from parse in case of a failure, not throw. That allows things like the firstMatch regex to work.
|
||||
return self.parse(input, in: index..<bounds.upperBound)
|
||||
}
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension RegexComponent where Self == Date.HTTPFormatStyle {
|
||||
/// Creates a regex component to match an HTTP date and time, such as "2015-11-14'T'15:05:03'Z'", and capture the string as a `Date` using the time zone as specified in the string.
|
||||
public static var http: Date.HTTPFormatStyle {
|
||||
return Date.HTTPFormatStyle()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Components
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
public extension FormatStyle where Self == DateComponents.HTTPFormatStyle {
|
||||
static var http: Self {
|
||||
return DateComponents.HTTPFormatStyle()
|
||||
}
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
public extension ParseableFormatStyle where Self == DateComponents.HTTPFormatStyle {
|
||||
static var http: Self { .init() }
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
public extension ParseStrategy where Self == DateComponents.HTTPFormatStyle {
|
||||
@_disfavoredOverload
|
||||
static var http: Self { .init() }
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents.HTTPFormatStyle : FormatStyle {
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents.HTTPFormatStyle : ParseStrategy {
|
||||
public var parseStrategy: DateComponents.HTTPFormatStyle { self }
|
||||
}
|
||||
|
||||
@available(FoundationPreview 6.2, *)
|
||||
extension DateComponents {
|
||||
/// Converts `DateComponents` into RFC 9110-compatible "HTTP date" `String`, and parses in the reverse direction.
|
||||
/// This parser does not do validation on the individual values of the components. An optional date can be created from the result using `Calendar(identifier: .gregorian).date(from: ...)`.
|
||||
/// When formatting, missing or invalid fields are filled with default values: `Sun`, `01`, `Jan`, `2000`, `00:00:00`, `GMT`. Note that missing fields may result in an invalid date or time. Other values in the `DateComponents` are ignored.
|
||||
public struct HTTPFormatStyle : Sendable, Hashable, Codable, ParseableFormatStyle {
|
||||
public init() {
|
||||
}
|
||||
|
||||
// MARK: - Format
|
||||
|
||||
public func format(_ components: DateComponents) -> String {
|
||||
let capacity = 32 // It is believed no HTTP date can exceed this size (max should be 26)
|
||||
return withUnsafeTemporaryAllocation(of: CChar.self, capacity: capacity + 1) { _buffer in
|
||||
var buffer = OutputBuffer(initializing: _buffer.baseAddress!, capacity: _buffer.count)
|
||||
|
||||
switch components.weekday {
|
||||
case 2:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "M")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "o")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "n")))
|
||||
case 3:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "T")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "u")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "e")))
|
||||
case 4:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "W")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "e")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "d")))
|
||||
case 5:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "T")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "h")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "u")))
|
||||
case 6:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "F")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "r")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "i")))
|
||||
case 7:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "S")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "a")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "t")))
|
||||
case 1:
|
||||
// Sunday, or default / missing
|
||||
fallthrough
|
||||
default:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "S")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "u")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "n")))
|
||||
}
|
||||
|
||||
buffer.appendElement(CChar(UInt8(ascii: ",")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: " ")))
|
||||
|
||||
let day = components.day ?? 1
|
||||
buffer.append(day, zeroPad: 2)
|
||||
buffer.appendElement(CChar(UInt8(ascii: " ")))
|
||||
|
||||
switch components.month {
|
||||
case 2:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "F")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "e")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "b")))
|
||||
case 3:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "M")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "a")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "r")))
|
||||
case 4:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "A")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "p")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "r")))
|
||||
case 5:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "M")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "a")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "y")))
|
||||
case 6:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "J")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "u")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "n")))
|
||||
case 7:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "J")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "u")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "l")))
|
||||
case 8:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "A")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "u")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "g")))
|
||||
case 9:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "S")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "e")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "p")))
|
||||
case 10:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "O")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "c")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "t")))
|
||||
case 11:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "N")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "o")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "v")))
|
||||
case 12:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "D")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "e")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "c")))
|
||||
case 1:
|
||||
// Jan or default value
|
||||
fallthrough
|
||||
default:
|
||||
buffer.appendElement(CChar(UInt8(ascii: "J")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "a")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "n")))
|
||||
}
|
||||
buffer.appendElement(CChar(UInt8(ascii: " ")))
|
||||
|
||||
let year = components.year ?? 2000
|
||||
buffer.append(year, zeroPad: 4)
|
||||
buffer.appendElement(CChar(UInt8(ascii: " ")))
|
||||
|
||||
let h = components.hour ?? 0
|
||||
let m = components.minute ?? 0
|
||||
let s = components.second ?? 0
|
||||
|
||||
buffer.append(h, zeroPad: 2)
|
||||
buffer.appendElement(CChar(UInt8(ascii: ":")))
|
||||
buffer.append(m, zeroPad: 2)
|
||||
buffer.appendElement(CChar(UInt8(ascii: ":")))
|
||||
buffer.append(s, zeroPad: 2)
|
||||
|
||||
buffer.appendElement(CChar(UInt8(ascii: " ")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "G")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "M")))
|
||||
buffer.appendElement(CChar(UInt8(ascii: "T")))
|
||||
|
||||
// Null-terminate
|
||||
buffer.appendElement(CChar(0))
|
||||
|
||||
// Make a string
|
||||
let initialized = buffer.relinquishBorrowedMemory()
|
||||
return String(validatingUTF8: initialized.baseAddress!)!
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Parse
|
||||
|
||||
fileprivate struct ComponentsParseResult {
|
||||
var consumed: Int
|
||||
var components: DateComponents
|
||||
}
|
||||
|
||||
public func parse(_ value: String) throws -> DateComponents {
|
||||
guard let (_, components) = parse(value, in: value.startIndex..<value.endIndex) else {
|
||||
throw parseError(value, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now))
|
||||
}
|
||||
return components
|
||||
}
|
||||
|
||||
private func parse(_ value: String, in range: Range<String.Index>) -> (String.Index, DateComponents)? {
|
||||
var v = value[range]
|
||||
guard !v.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = v.withUTF8 { buffer -> (Int, DateComponents)? in
|
||||
let view = BufferView(unsafeBufferPointer: buffer)!
|
||||
|
||||
guard let comps = try? components(from: value, in: view) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (comps.consumed, comps.components)
|
||||
}
|
||||
|
||||
guard let result else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let endIndex = value.utf8.index(v.startIndex, offsetBy: result.0)
|
||||
return (endIndex, result.1)
|
||||
}
|
||||
|
||||
fileprivate func components(from inputString: String, in view: borrowing BufferView<UInt8>) throws -> ComponentsParseResult {
|
||||
// https://www.rfc-editor.org/rfc/rfc9110.html#http.date
|
||||
// <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
|
||||
|
||||
var it = view.makeIterator()
|
||||
var dc = DateComponents()
|
||||
|
||||
// Despite the spec, we allow the weekday name to be optional.
|
||||
guard let maybeWeekday1 = it.peek() else {
|
||||
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now))
|
||||
}
|
||||
|
||||
if isASCIIDigit(maybeWeekday1) {
|
||||
// This is the first digit of the day. Weekday is not present.
|
||||
} else {
|
||||
// Anything else must be a day-name (Mon, Tue, ... Sun)
|
||||
guard let weekday1 = it.next(), let weekday2 = it.next(), let weekday3 = it.next() else {
|
||||
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now))
|
||||
}
|
||||
|
||||
dc.weekday = switch (weekday1, weekday2, weekday3) {
|
||||
case (UInt8(ascii: "S"), UInt8(ascii: "u"), UInt8(ascii: "n")):
|
||||
1
|
||||
case (UInt8(ascii: "M"), UInt8(ascii: "o"), UInt8(ascii: "n")):
|
||||
2
|
||||
case (UInt8(ascii: "T"), UInt8(ascii: "u"), UInt8(ascii: "e")):
|
||||
3
|
||||
case (UInt8(ascii: "W"), UInt8(ascii: "e"), UInt8(ascii: "d")):
|
||||
4
|
||||
case (UInt8(ascii: "T"), UInt8(ascii: "h"), UInt8(ascii: "u")):
|
||||
5
|
||||
case (UInt8(ascii: "F"), UInt8(ascii: "r"), UInt8(ascii: "i")):
|
||||
6
|
||||
case (UInt8(ascii: "S"), UInt8(ascii: "a"), UInt8(ascii: "t")):
|
||||
7
|
||||
default:
|
||||
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Malformed weekday name")
|
||||
}
|
||||
|
||||
// Move past , and space to weekday
|
||||
try it.expectCharacter(UInt8(ascii: ","), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing , after weekday")
|
||||
try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing space after weekday")
|
||||
}
|
||||
|
||||
dc.day = try it.digits(minDigits: 2, maxDigits: 2, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing or malformed day")
|
||||
try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
|
||||
// month-name (Jan, Feb, ... Dec)
|
||||
guard let month1 = it.next(), let month2 = it.next(), let month3 = it.next() else {
|
||||
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing month")
|
||||
}
|
||||
|
||||
dc.month = switch (month1, month2, month3) {
|
||||
case (UInt8(ascii: "J"), UInt8(ascii: "a"), UInt8(ascii: "n")):
|
||||
1
|
||||
case (UInt8(ascii: "F"), UInt8(ascii: "e"), UInt8(ascii: "b")):
|
||||
2
|
||||
case (UInt8(ascii: "M"), UInt8(ascii: "a"), UInt8(ascii: "r")):
|
||||
3
|
||||
case (UInt8(ascii: "A"), UInt8(ascii: "p"), UInt8(ascii: "r")):
|
||||
4
|
||||
case (UInt8(ascii: "M"), UInt8(ascii: "a"), UInt8(ascii: "y")):
|
||||
5
|
||||
case (UInt8(ascii: "J"), UInt8(ascii: "u"), UInt8(ascii: "n")):
|
||||
6
|
||||
case (UInt8(ascii: "J"), UInt8(ascii: "u"), UInt8(ascii: "l")):
|
||||
7
|
||||
case (UInt8(ascii: "A"), UInt8(ascii: "u"), UInt8(ascii: "g")):
|
||||
8
|
||||
case (UInt8(ascii: "S"), UInt8(ascii: "e"), UInt8(ascii: "p")):
|
||||
9
|
||||
case (UInt8(ascii: "O"), UInt8(ascii: "c"), UInt8(ascii: "t")):
|
||||
10
|
||||
case (UInt8(ascii: "N"), UInt8(ascii: "o"), UInt8(ascii: "v")):
|
||||
11
|
||||
case (UInt8(ascii: "D"), UInt8(ascii: "e"), UInt8(ascii: "c")):
|
||||
12
|
||||
default:
|
||||
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Month \(String(describing: dc.month)) is out of bounds")
|
||||
}
|
||||
|
||||
try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
|
||||
dc.year = try it.digits(minDigits: 4, maxDigits: 4, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
|
||||
let hour = try it.digits(minDigits: 2, maxDigits: 2, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
if hour < 0 || hour > 23 {
|
||||
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Hour \(hour) is out of bounds")
|
||||
}
|
||||
dc.hour = hour
|
||||
|
||||
try it.expectCharacter(UInt8(ascii: ":"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
let minute = try it.digits(minDigits: 2, maxDigits: 2, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
if minute < 0 || minute > 59 {
|
||||
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Minute \(minute) is out of bounds")
|
||||
}
|
||||
dc.minute = minute
|
||||
|
||||
try it.expectCharacter(UInt8(ascii: ":"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
let second = try it.digits(minDigits: 2, maxDigits: 2, input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
// second '60' is supported in the spec for leap seconds, but Foundation does not support leap seconds. 60 is adjusted to 59.
|
||||
if second < 0 || second > 60 {
|
||||
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Second \(second) is out of bounds")
|
||||
}
|
||||
// Foundation does not support leap seconds. We convert 60 seconds into 59 seconds.
|
||||
if second == 60 {
|
||||
dc.second = 59
|
||||
} else {
|
||||
dc.second = second
|
||||
}
|
||||
try it.expectCharacter(UInt8(ascii: " "), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
|
||||
|
||||
// "GMT"
|
||||
try it.expectCharacter(UInt8(ascii: "G"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing GMT time zone")
|
||||
try it.expectCharacter(UInt8(ascii: "M"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing GMT time zone")
|
||||
try it.expectCharacter(UInt8(ascii: "T"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Missing GMT time zone")
|
||||
|
||||
// Time zone is always GMT, calendar is always Gregorian
|
||||
dc.timeZone = .gmt
|
||||
dc.calendar = Calendar(identifier: .gregorian)
|
||||
|
||||
// Would be nice to see this functionality on BufferView, but for now we calculate it ourselves.
|
||||
let utf8CharactersRead = it.curPointer - view.startIndex._rawValue
|
||||
|
||||
return ComponentsParseResult(consumed: utf8CharactersRead, components: dc)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -302,52 +302,6 @@ extension Date.ISO8601FormatStyle : FormatStyle {
|
||||
let result = withUnsafeTemporaryAllocation(of: CChar.self, capacity: capacity + 1) { _buffer in
|
||||
var buffer = OutputBuffer(initializing: _buffer.baseAddress!, capacity: _buffer.count)
|
||||
|
||||
let asciiZero = CChar(48)
|
||||
|
||||
func append(_ i: Int, zeroPad: Int, buffer: inout OutputBuffer<CChar>) {
|
||||
if i < 10 {
|
||||
if zeroPad - 1 > 0 {
|
||||
for _ in 0..<zeroPad-1 { buffer.appendElement(asciiZero) }
|
||||
}
|
||||
buffer.appendElement(asciiZero + CChar(i))
|
||||
} else if i < 100 {
|
||||
if zeroPad - 2 > 0 {
|
||||
for _ in 0..<zeroPad-2 { buffer.appendElement(asciiZero) }
|
||||
}
|
||||
let (tens, ones) = i.quotientAndRemainder(dividingBy: 10)
|
||||
buffer.appendElement(asciiZero + CChar(tens))
|
||||
buffer.appendElement(asciiZero + CChar(ones))
|
||||
} else if i < 1000 {
|
||||
if zeroPad - 3 > 0 {
|
||||
for _ in 0..<zeroPad-3 { buffer.appendElement(asciiZero) }
|
||||
}
|
||||
let (hundreds, remainder) = i.quotientAndRemainder(dividingBy: 100)
|
||||
let (tens, ones) = remainder.quotientAndRemainder(dividingBy: 10)
|
||||
buffer.appendElement(asciiZero + CChar(hundreds))
|
||||
buffer.appendElement(asciiZero + CChar(tens))
|
||||
buffer.appendElement(asciiZero + CChar(ones))
|
||||
} else if i < 10000 {
|
||||
if zeroPad - 4 > 0 {
|
||||
for _ in 0..<zeroPad-4 { buffer.appendElement(asciiZero) }
|
||||
}
|
||||
let (thousands, remainder) = i.quotientAndRemainder(dividingBy: 1000)
|
||||
let (hundreds, remainder2) = remainder.quotientAndRemainder(dividingBy: 100)
|
||||
let (tens, ones) = remainder2.quotientAndRemainder(dividingBy: 10)
|
||||
buffer.appendElement(asciiZero + CChar(thousands))
|
||||
buffer.appendElement(asciiZero + CChar(hundreds))
|
||||
buffer.appendElement(asciiZero + CChar(tens))
|
||||
buffer.appendElement(asciiZero + CChar(ones))
|
||||
} else {
|
||||
// Special case - we don't do zero padding
|
||||
var desc = i.numericStringRepresentation
|
||||
desc.withUTF8 {
|
||||
$0.withMemoryRebound(to: CChar.self) { buf in
|
||||
buffer.append(fromContentsOf: buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let asciiColon = CChar(58)
|
||||
let asciiDash = CChar(45)
|
||||
let asciiSpace = CChar(32)
|
||||
@ -361,7 +315,7 @@ extension Date.ISO8601FormatStyle : FormatStyle {
|
||||
|
||||
if formatFields.contains(.year) {
|
||||
if formatFields.contains(.weekOfYear), let y = components.yearForWeekOfYear {
|
||||
append(y, zeroPad: 4, buffer: &buffer)
|
||||
buffer.append(y, zeroPad: 4)
|
||||
} else {
|
||||
var y = components.year!
|
||||
if let era = components.era, era == 0 {
|
||||
@ -371,7 +325,7 @@ extension Date.ISO8601FormatStyle : FormatStyle {
|
||||
buffer.appendElement(asciiMinus)
|
||||
y = -y
|
||||
}
|
||||
append(y, zeroPad: 4, buffer: &buffer)
|
||||
buffer.append(y, zeroPad: 4)
|
||||
}
|
||||
|
||||
needSeparator = true
|
||||
@ -382,7 +336,7 @@ extension Date.ISO8601FormatStyle : FormatStyle {
|
||||
buffer.appendElement(asciiDash)
|
||||
}
|
||||
let m = components.month!
|
||||
append(m, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(m, zeroPad: 2)
|
||||
needSeparator = true
|
||||
}
|
||||
|
||||
@ -392,7 +346,7 @@ extension Date.ISO8601FormatStyle : FormatStyle {
|
||||
}
|
||||
let woy = components.weekOfYear!
|
||||
buffer.appendElement(asciiWeekOfYearSeparator)
|
||||
append(woy, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(woy, zeroPad: 2)
|
||||
needSeparator = true
|
||||
}
|
||||
|
||||
@ -407,13 +361,13 @@ extension Date.ISO8601FormatStyle : FormatStyle {
|
||||
if weekday >= 10 {
|
||||
weekday = 10
|
||||
}
|
||||
append(weekday - 1, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(weekday - 1, zeroPad: 2)
|
||||
} else if formatFields.contains(.month) {
|
||||
let day = components.day!
|
||||
append(day, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(day, zeroPad: 2)
|
||||
} else {
|
||||
let dayOfYear = components.dayOfYear!
|
||||
append(dayOfYear, zeroPad: 3, buffer: &buffer)
|
||||
buffer.append(dayOfYear, zeroPad: 3)
|
||||
}
|
||||
|
||||
needSeparator = true
|
||||
@ -433,22 +387,22 @@ extension Date.ISO8601FormatStyle : FormatStyle {
|
||||
|
||||
switch timeSeparator {
|
||||
case .colon:
|
||||
append(h, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(h, zeroPad: 2)
|
||||
buffer.appendElement(asciiColon)
|
||||
append(m, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(m, zeroPad: 2)
|
||||
buffer.appendElement(asciiColon)
|
||||
append(s, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(s, zeroPad: 2)
|
||||
case .omitted:
|
||||
append(h, zeroPad: 2, buffer: &buffer)
|
||||
append(m, zeroPad: 2, buffer: &buffer)
|
||||
append(s, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(h, zeroPad: 2)
|
||||
buffer.append(m, zeroPad: 2)
|
||||
buffer.append(s, zeroPad: 2)
|
||||
}
|
||||
|
||||
if includingFractionalSeconds {
|
||||
let ns = components.nanosecond!
|
||||
let ms = Int((Double(ns) / 1_000_000.0).rounded(.towardZero))
|
||||
buffer.appendElement(asciiPeriod)
|
||||
append(ms, zeroPad: 3, buffer: &buffer)
|
||||
buffer.append(ms, zeroPad: 3)
|
||||
}
|
||||
|
||||
needSeparator = true
|
||||
@ -474,16 +428,16 @@ extension Date.ISO8601FormatStyle : FormatStyle {
|
||||
} else {
|
||||
buffer.appendElement(asciiPlus)
|
||||
}
|
||||
append(hour, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(hour, zeroPad: 2)
|
||||
if timeZoneSeparator == .colon {
|
||||
buffer.appendElement(asciiColon)
|
||||
}
|
||||
append(minute, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(minute, zeroPad: 2)
|
||||
if second != 0 {
|
||||
if timeZoneSeparator == .colon {
|
||||
buffer.appendElement(asciiColon)
|
||||
}
|
||||
append(second, zeroPad: 2, buffer: &buffer)
|
||||
buffer.append(second, zeroPad: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -510,86 +464,12 @@ extension Date.ISO8601FormatStyle {
|
||||
private func components(from inputString: String, in view: borrowing BufferView<UInt8>) throws -> ComponentsParseResult {
|
||||
let fields = formatFields
|
||||
|
||||
let asciiDash : UInt8 = 45 // -
|
||||
let asciiW : UInt8 = 87 // W
|
||||
let asciiT : UInt8 = 84 // T
|
||||
let asciiZero : UInt8 = 48 // 0
|
||||
let asciiNine : UInt8 = 57 // 9
|
||||
let asciiSpace : UInt8 = 32 // space
|
||||
let asciiColon : UInt8 = 58 // :
|
||||
let asciiPeriod : UInt8 = 46 // .
|
||||
let asciiMinus : UInt8 = 45 // same as -
|
||||
let asciiPlus : UInt8 = 43 // +
|
||||
|
||||
func isDigit(_ x: UInt8) -> Bool {
|
||||
x >= asciiZero && x <= asciiNine
|
||||
}
|
||||
|
||||
func expectCharacter(_ expected: UInt8, _ i: inout BufferView<UInt8>.Iterator) throws {
|
||||
guard let parsed = i.next(), parsed == expected else {
|
||||
throw parseError(inputString, exampleFormattedString: self.format(Date.now))
|
||||
}
|
||||
}
|
||||
|
||||
func expectOneOrMoreCharacters(_ expected: UInt8, _ i: inout BufferView<UInt8>.Iterator) throws {
|
||||
guard let parsed = i.next(), parsed == expected else {
|
||||
throw parseError(inputString, exampleFormattedString: self.format(Date.now))
|
||||
}
|
||||
|
||||
while let parsed = i.peek(), parsed == expected {
|
||||
i.advance()
|
||||
}
|
||||
}
|
||||
|
||||
func expectZeroOrMoreCharacters(_ expected: UInt8, _ i: inout BufferView<UInt8>.Iterator) {
|
||||
while let parsed = i.peek(), parsed == expected {
|
||||
i.advance()
|
||||
}
|
||||
}
|
||||
|
||||
func digits(maxDigits: Int? = nil, nanoseconds: Bool = false, _ i: inout BufferView<UInt8>.Iterator) throws -> Int {
|
||||
// Consume all leading zeros, parse until we no longer see a digit
|
||||
var result = 0
|
||||
var count = 0
|
||||
// Cap at 10 digits max to avoid overflow
|
||||
let max = min(maxDigits ?? 10, 10)
|
||||
while let next = i.peek(), isDigit(next) {
|
||||
let digit = Int(next - asciiZero)
|
||||
result *= 10
|
||||
result += digit
|
||||
i.advance()
|
||||
count += 1
|
||||
if count >= max { break }
|
||||
}
|
||||
|
||||
guard count > 0 else {
|
||||
// No digits actually found
|
||||
throw parseError(inputString, exampleFormattedString: self.format(Date.now))
|
||||
}
|
||||
|
||||
if nanoseconds {
|
||||
// Keeps us in the land of integers
|
||||
if count == 1 { return result * 100_000_000 }
|
||||
if count == 2 { return result * 10_000_000 }
|
||||
if count == 3 { return result * 1_000_000 }
|
||||
if count == 4 { return result * 100_000 }
|
||||
if count == 5 { return result * 10_000 }
|
||||
if count == 6 { return result * 1_000 }
|
||||
if count == 7 { return result * 100 }
|
||||
if count == 8 { return result * 10 }
|
||||
if count == 9 { return result }
|
||||
throw parseError(inputString, exampleFormattedString: self.format(Date.now))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var it = view.makeIterator()
|
||||
var needsSeparator = false
|
||||
var dc = DateComponents()
|
||||
if fields.contains(.year) {
|
||||
let max = dateSeparator == .omitted ? 4 : nil
|
||||
let value = try digits(maxDigits: max, &it)
|
||||
let value = try it.digits(maxDigits: max, input: inputString, onFailure: self.format(Date.now))
|
||||
if fields.contains(.weekOfYear) {
|
||||
dc.yearForWeekOfYear = value
|
||||
} else {
|
||||
@ -604,12 +484,12 @@ extension Date.ISO8601FormatStyle {
|
||||
|
||||
if fields.contains(.month) {
|
||||
if needsSeparator && dateSeparator == .dash {
|
||||
try expectCharacter(asciiDash, &it)
|
||||
try it.expectCharacter(UInt8(ascii: "-"), input: inputString, onFailure: self.format(Date.now))
|
||||
}
|
||||
|
||||
// parse month digits
|
||||
let max = dateSeparator == .omitted ? 2 : nil
|
||||
let value = try digits(maxDigits: max, &it)
|
||||
let value = try it.digits(maxDigits: max, input: inputString, onFailure: self.format(Date.now))
|
||||
guard _calendar.maximumRange(of: .month)!.contains(value) else {
|
||||
throw parseError(inputString, exampleFormattedString: self.format(Date.now))
|
||||
}
|
||||
@ -618,14 +498,14 @@ extension Date.ISO8601FormatStyle {
|
||||
needsSeparator = true
|
||||
} else if fields.contains(.weekOfYear) {
|
||||
if needsSeparator && dateSeparator == .dash {
|
||||
try expectCharacter(asciiDash, &it)
|
||||
try it.expectCharacter(UInt8(ascii: "-"), input: inputString, onFailure: self.format(Date.now))
|
||||
}
|
||||
// parse W
|
||||
try expectCharacter(asciiW, &it)
|
||||
try it.expectCharacter(UInt8(ascii: "W"), input: inputString, onFailure: self.format(Date.now))
|
||||
|
||||
// parse week of year digits
|
||||
let max = dateSeparator == .omitted ? 2 : nil
|
||||
let value = try digits(maxDigits: max, &it)
|
||||
let value = try it.digits(maxDigits: max, input: inputString, onFailure: self.format(Date.now))
|
||||
guard _calendar.maximumRange(of: .weekOfYear)!.contains(value) else {
|
||||
throw parseError(inputString, exampleFormattedString: self.format(Date.now))
|
||||
}
|
||||
@ -639,14 +519,14 @@ extension Date.ISO8601FormatStyle {
|
||||
|
||||
if fields.contains(.day) {
|
||||
if needsSeparator && dateSeparator == .dash {
|
||||
try expectCharacter(asciiDash, &it)
|
||||
try it.expectCharacter(UInt8(ascii: "-"), input: inputString, onFailure: self.format(Date.now))
|
||||
}
|
||||
|
||||
if fields.contains(.weekOfYear) {
|
||||
// parse day of week ('ee')
|
||||
// ISO8601 "1" is Monday. For our date components, 2 is Monday. Add 1 to account for difference.
|
||||
let max = dateSeparator == .omitted ? 2 : nil
|
||||
let value = (try digits(maxDigits: max, &it) % 7) + 1
|
||||
let value = (try it.digits(maxDigits: max, input: inputString, onFailure: self.format(Date.now)) % 7) + 1
|
||||
|
||||
guard _calendar.maximumRange(of: .weekday)!.contains(value) else {
|
||||
throw parseError(inputString, exampleFormattedString: self.format(Date.now))
|
||||
@ -656,7 +536,7 @@ extension Date.ISO8601FormatStyle {
|
||||
} else if fields.contains(.month) {
|
||||
// parse day of month ('dd')
|
||||
let max = dateSeparator == .omitted ? 2 : nil
|
||||
let value = try digits(maxDigits: max, &it)
|
||||
let value = try it.digits(maxDigits: max, input: inputString, onFailure: self.format(Date.now))
|
||||
guard _calendar.maximumRange(of: .day)!.contains(value) else {
|
||||
throw parseError(inputString, exampleFormattedString: self.format(Date.now))
|
||||
}
|
||||
@ -666,7 +546,7 @@ extension Date.ISO8601FormatStyle {
|
||||
} else {
|
||||
// parse 3 digit day of year ('DDD')
|
||||
let max = dateSeparator == .omitted ? 3 : nil
|
||||
let value = try digits(maxDigits: max, &it)
|
||||
let value = try it.digits(maxDigits: max, input: inputString, onFailure: self.format(Date.now))
|
||||
guard _calendar.maximumRange(of: .dayOfYear)!.contains(value) else {
|
||||
throw parseError(inputString, exampleFormattedString: self.format(Date.now))
|
||||
}
|
||||
@ -682,30 +562,30 @@ extension Date.ISO8601FormatStyle {
|
||||
switch dateTimeSeparator {
|
||||
case .standard:
|
||||
// parse T
|
||||
try expectCharacter(asciiT, &it)
|
||||
try it.expectCharacter(UInt8(ascii: "T"), input: inputString, onFailure: self.format(Date.now))
|
||||
case .space:
|
||||
// parse any number of spaces
|
||||
try expectOneOrMoreCharacters(asciiSpace, &it)
|
||||
try it.expectOneOrMoreCharacters(UInt8(ascii: " "), input: inputString, onFailure: self.format(Date.now))
|
||||
}
|
||||
}
|
||||
|
||||
switch timeSeparator {
|
||||
case .colon:
|
||||
dc.hour = try digits(&it)
|
||||
try expectCharacter(asciiColon, &it)
|
||||
dc.minute = try digits(&it)
|
||||
try expectCharacter(asciiColon, &it)
|
||||
dc.second = try digits(&it)
|
||||
dc.hour = try it.digits(input: inputString, onFailure: self.format(Date.now))
|
||||
try it.expectCharacter(UInt8(ascii: ":"), input: inputString, onFailure: self.format(Date.now))
|
||||
dc.minute = try it.digits(input: inputString, onFailure: self.format(Date.now))
|
||||
try it.expectCharacter(UInt8(ascii: ":"), input: inputString, onFailure: self.format(Date.now))
|
||||
dc.second = try it.digits(input: inputString, onFailure: self.format(Date.now))
|
||||
case .omitted:
|
||||
dc.hour = try digits(maxDigits: 2, &it)
|
||||
dc.minute = try digits(maxDigits: 2, &it)
|
||||
dc.second = try digits(maxDigits: 2, &it)
|
||||
dc.hour = try it.digits(maxDigits: 2, input: inputString, onFailure: self.format(Date.now))
|
||||
dc.minute = try it.digits(maxDigits: 2, input: inputString, onFailure: self.format(Date.now))
|
||||
dc.second = try it.digits(maxDigits: 2, input: inputString, onFailure: self.format(Date.now))
|
||||
}
|
||||
|
||||
if includingFractionalSeconds {
|
||||
try expectCharacter(asciiPeriod, &it)
|
||||
try it.expectCharacter(UInt8(ascii: "."), input: inputString, onFailure: self.format(Date.now))
|
||||
|
||||
let fractionalSeconds = try digits(nanoseconds: true, &it)
|
||||
let fractionalSeconds = try it.digits(nanoseconds: true, input: inputString, onFailure: self.format(Date.now))
|
||||
dc.nanosecond = fractionalSeconds
|
||||
}
|
||||
|
||||
@ -715,7 +595,7 @@ extension Date.ISO8601FormatStyle {
|
||||
if fields.contains(.timeZone) {
|
||||
// For compatibility with ICU implementation, if the dateTimeSeparator is a space, consume any number (including zero) of spaces here.
|
||||
if dateTimeSeparator == .space {
|
||||
expectZeroOrMoreCharacters(asciiSpace, &it)
|
||||
it.expectZeroOrMoreCharacters(UInt8(ascii: " "))
|
||||
}
|
||||
|
||||
guard let plusOrMinusOrZ = it.next() else {
|
||||
@ -737,8 +617,8 @@ extension Date.ISO8601FormatStyle {
|
||||
let m = it.next(), (m == UInt8(ascii: "M") || m == UInt8(ascii: "m")),
|
||||
let t = it.next(), (t == UInt8(ascii: "T") || t == UInt8(ascii: "t")) {
|
||||
// Allow GMT followed by + or -, or end of string, or other
|
||||
if let next = it.peek(), (next == asciiPlus || next == asciiMinus) {
|
||||
if next == asciiPlus { positive = true }
|
||||
if let next = it.peek(), (next == UInt8(ascii: "+") || next == UInt8(ascii: "-")) {
|
||||
if next == UInt8(ascii: "+") { positive = true }
|
||||
else { positive = false }
|
||||
it.advance()
|
||||
} else {
|
||||
@ -750,8 +630,8 @@ extension Date.ISO8601FormatStyle {
|
||||
let t = it.next(), (t == UInt8(ascii: "T") || t == UInt8(ascii: "t")),
|
||||
let c = it.next(), (c == UInt8(ascii: "C") || c == UInt8(ascii: "c")) {
|
||||
// Allow UTC followed by + or -, or end of string, or other
|
||||
if let next = it.peek(), (next == asciiPlus || next == asciiMinus) {
|
||||
if next == asciiPlus { positive = true }
|
||||
if let next = it.peek(), (next == UInt8(ascii: "+") || next == UInt8(ascii: "-")) {
|
||||
if next == UInt8(ascii: "+") { positive = true }
|
||||
else { positive = false }
|
||||
it.advance()
|
||||
} else {
|
||||
@ -759,9 +639,9 @@ extension Date.ISO8601FormatStyle {
|
||||
tzOffset = 0
|
||||
skipDigits = true
|
||||
}
|
||||
} else if plusOrMinusOrZ == asciiPlus {
|
||||
} else if plusOrMinusOrZ == UInt8(ascii: "+") {
|
||||
positive = true
|
||||
} else if plusOrMinusOrZ == asciiMinus {
|
||||
} else if plusOrMinusOrZ == UInt8(ascii: "-") {
|
||||
positive = false
|
||||
} else {
|
||||
// Expected time zone, found garbage
|
||||
@ -773,24 +653,24 @@ extension Date.ISO8601FormatStyle {
|
||||
|
||||
// parse Time Zone: ISO8601 extended hms?, with Z
|
||||
// examples: -08:00, -07:52:58, Z
|
||||
let hours = try digits(maxDigits: 2, &it)
|
||||
let hours = try it.digits(maxDigits: 2, input: inputString, onFailure: self.format(Date.now))
|
||||
|
||||
// Expect a colon, or not
|
||||
if let maybeColon = it.peek(), maybeColon == asciiColon {
|
||||
if let maybeColon = it.peek(), maybeColon == UInt8(ascii: ":") {
|
||||
// Throw it away
|
||||
it.advance()
|
||||
}
|
||||
|
||||
let minutes = try digits(maxDigits: 2, &it)
|
||||
let minutes = try it.digits(maxDigits: 2, input: inputString, onFailure: self.format(Date.now))
|
||||
|
||||
if let maybeColon = it.peek(), maybeColon == asciiColon {
|
||||
if let maybeColon = it.peek(), maybeColon == UInt8(ascii: ":") {
|
||||
// Throw it away
|
||||
it.advance()
|
||||
}
|
||||
|
||||
if let secondsTens = it.peek(), isDigit(secondsTens) {
|
||||
if let secondsTens = it.peek(), isASCIIDigit(secondsTens) {
|
||||
// We have seconds
|
||||
let seconds = try digits(maxDigits: 2, &it)
|
||||
let seconds = try it.digits(maxDigits: 2, input: inputString, onFailure: self.format(Date.now))
|
||||
tzOffset = (hours * 3600) + (minutes * 60) + seconds
|
||||
} else {
|
||||
// If the next character is missing, that's allowed - the time can be something like just -0852 and then the string can end
|
||||
|
@ -10,13 +10,136 @@
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
package func parseError(_ value: String, exampleFormattedString: String?) -> CocoaError {
|
||||
package func parseError(_ value: String, exampleFormattedString: String?, extendedDescription: String? = nil) -> CocoaError {
|
||||
let errorStr: String
|
||||
if let exampleFormattedString = exampleFormattedString {
|
||||
errorStr = "Cannot parse \(value). String should adhere to the preferred format of the locale, such as \(exampleFormattedString)."
|
||||
errorStr = "Cannot parse \(value)\(extendedDescription.map({ ": \($0)." }) ?? ".") String should adhere to the preferred format of the locale, such as \(exampleFormattedString)."
|
||||
} else {
|
||||
errorStr = "Cannot parse \(value)."
|
||||
errorStr = "Cannot parse \(value)\(extendedDescription.map({ ": \($0)." }) ?? ".")"
|
||||
}
|
||||
return CocoaError(CocoaError.formatting, userInfo: [ NSDebugDescriptionErrorKey: errorStr ])
|
||||
}
|
||||
|
||||
func isASCIIDigit(_ x: UInt8) -> Bool {
|
||||
x >= UInt8(ascii: "0") && x <= UInt8(ascii: "9")
|
||||
}
|
||||
|
||||
extension BufferViewIterator<UInt8> {
|
||||
mutating func expectCharacter(_ expected: UInt8, input: String, onFailure: @autoclosure () -> (String), extendedDescription: String? = nil) throws {
|
||||
guard let parsed = next(), parsed == expected else {
|
||||
throw parseError(input, exampleFormattedString: onFailure(), extendedDescription: extendedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func expectOneOrMoreCharacters(_ expected: UInt8, input: String, onFailure: @autoclosure () -> (String), extendedDescription: String? = nil) throws {
|
||||
guard let parsed = next(), parsed == expected else {
|
||||
throw parseError(input, exampleFormattedString: onFailure(), extendedDescription: extendedDescription)
|
||||
}
|
||||
|
||||
while let parsed = peek(), parsed == expected {
|
||||
advance()
|
||||
}
|
||||
}
|
||||
|
||||
mutating func expectZeroOrMoreCharacters(_ expected: UInt8) {
|
||||
while let parsed = peek(), parsed == expected {
|
||||
advance()
|
||||
}
|
||||
}
|
||||
|
||||
mutating func digits(minDigits: Int? = nil, maxDigits: Int? = nil, nanoseconds: Bool = false, input: String, onFailure: @autoclosure () -> (String), extendedDescription: String? = nil) throws -> Int {
|
||||
// Consume all leading zeros, parse until we no longer see a digit
|
||||
var result = 0
|
||||
var count = 0
|
||||
// Cap at 10 digits max to avoid overflow
|
||||
let max = min(maxDigits ?? 10, 10)
|
||||
while let next = peek(), isASCIIDigit(next) {
|
||||
let digit = Int(next - UInt8(ascii: "0"))
|
||||
result *= 10
|
||||
result += digit
|
||||
advance()
|
||||
count += 1
|
||||
if count >= max { break }
|
||||
}
|
||||
|
||||
guard count > 0 else {
|
||||
// No digits actually found
|
||||
throw parseError(input, exampleFormattedString: onFailure(), extendedDescription: extendedDescription)
|
||||
}
|
||||
|
||||
if let minDigits, count < minDigits {
|
||||
// Too few digits found
|
||||
throw parseError(input, exampleFormattedString: onFailure(), extendedDescription: extendedDescription)
|
||||
}
|
||||
|
||||
if nanoseconds {
|
||||
// Keeps us in the land of integers
|
||||
if count == 1 { return result * 100_000_000 }
|
||||
if count == 2 { return result * 10_000_000 }
|
||||
if count == 3 { return result * 1_000_000 }
|
||||
if count == 4 { return result * 100_000 }
|
||||
if count == 5 { return result * 10_000 }
|
||||
if count == 6 { return result * 1_000 }
|
||||
if count == 7 { return result * 100 }
|
||||
if count == 8 { return result * 10 }
|
||||
if count == 9 { return result }
|
||||
throw parseError(input, exampleFormattedString: onFailure(), extendedDescription: extendedDescription)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Formatting helpers
|
||||
extension OutputBuffer<CChar> {
|
||||
static let asciiZero = CChar(48)
|
||||
|
||||
mutating func append(_ i: Int, zeroPad: Int) {
|
||||
if i < 10 {
|
||||
if zeroPad - 1 > 0 {
|
||||
for _ in 0..<zeroPad-1 { appendElement(Self.asciiZero) }
|
||||
}
|
||||
appendElement(Self.asciiZero + CChar(i))
|
||||
} else if i < 100 {
|
||||
if zeroPad - 2 > 0 {
|
||||
for _ in 0..<zeroPad-2 { appendElement(Self.asciiZero) }
|
||||
}
|
||||
let (tens, ones) = i.quotientAndRemainder(dividingBy: 10)
|
||||
appendElement(Self.asciiZero + CChar(tens))
|
||||
appendElement(Self.asciiZero + CChar(ones))
|
||||
} else if i < 1000 {
|
||||
if zeroPad - 3 > 0 {
|
||||
for _ in 0..<zeroPad-3 { appendElement(Self.asciiZero) }
|
||||
}
|
||||
let (hundreds, remainder) = i.quotientAndRemainder(dividingBy: 100)
|
||||
let (tens, ones) = remainder.quotientAndRemainder(dividingBy: 10)
|
||||
appendElement(Self.asciiZero + CChar(hundreds))
|
||||
appendElement(Self.asciiZero + CChar(tens))
|
||||
appendElement(Self.asciiZero + CChar(ones))
|
||||
} else if i < 10000 {
|
||||
if zeroPad - 4 > 0 {
|
||||
for _ in 0..<zeroPad-4 { appendElement(Self.asciiZero) }
|
||||
}
|
||||
let (thousands, remainder) = i.quotientAndRemainder(dividingBy: 1000)
|
||||
let (hundreds, remainder2) = remainder.quotientAndRemainder(dividingBy: 100)
|
||||
let (tens, ones) = remainder2.quotientAndRemainder(dividingBy: 10)
|
||||
appendElement(Self.asciiZero + CChar(thousands))
|
||||
appendElement(Self.asciiZero + CChar(hundreds))
|
||||
appendElement(Self.asciiZero + CChar(tens))
|
||||
appendElement(Self.asciiZero + CChar(ones))
|
||||
} else {
|
||||
// Special case - we don't do zero padding
|
||||
var desc = i.numericStringRepresentation
|
||||
desc.withUTF8 {
|
||||
$0.withMemoryRebound(to: CChar.self) { buf in
|
||||
append(fromContentsOf: buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -679,7 +679,7 @@ extension JSONDecoderImpl: Decoder {
|
||||
var data: Data?
|
||||
if isSimple {
|
||||
data = withBuffer(for: region) { buffer, _ in
|
||||
try? Base64.decode(bytes: buffer)
|
||||
Data(decodingBase64: buffer)
|
||||
}
|
||||
}
|
||||
if data == nil {
|
||||
|
@ -271,65 +271,4 @@ extension BidirectionalCollection where Element == Unicode.Scalar, Index == Stri
|
||||
|
||||
}
|
||||
|
||||
// START: Workaround for https://github.com/swiftlang/swift/pull/78697
|
||||
|
||||
// The extensions below are temporarily relocated to work around a compiler crash.
|
||||
// Once that crash is resolved, they should be moved back to their original files.
|
||||
|
||||
#if !NO_FILESYSTEM
|
||||
|
||||
// Relocated from FileManager+Utilities.swift
|
||||
#if FOUNDATION_FRAMEWORK && os(macOS)
|
||||
extension URLResourceKey {
|
||||
static var _finderInfoKey: Self { URLResourceKey("_NSURLFinderInfoKey") }
|
||||
}
|
||||
#endif
|
||||
|
||||
// Relocated from FileManager+Files.swift
|
||||
#if FOUNDATION_FRAMEWORK
|
||||
internal import DarwinPrivate.sys.content_protection
|
||||
#endif
|
||||
|
||||
#if !os(Windows)
|
||||
#if FOUNDATION_FRAMEWORK
|
||||
extension FileProtectionType {
|
||||
var intValue: Int32? {
|
||||
switch self {
|
||||
case .complete: PROTECTION_CLASS_A
|
||||
case .init(rawValue: "NSFileProtectionWriteOnly"), .completeUnlessOpen: PROTECTION_CLASS_B
|
||||
case .init(rawValue: "NSFileProtectionCompleteUntilUserAuthentication"), .completeUntilFirstUserAuthentication: PROTECTION_CLASS_C
|
||||
case .none: PROTECTION_CLASS_D
|
||||
#if !os(macOS)
|
||||
case .completeWhenUserInactive: PROTECTION_CLASS_CX
|
||||
#endif
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
init?(intValue value: Int32) {
|
||||
switch value {
|
||||
case PROTECTION_CLASS_A: self = .complete
|
||||
case PROTECTION_CLASS_B: self = .completeUnlessOpen
|
||||
case PROTECTION_CLASS_C: self = .completeUntilFirstUserAuthentication
|
||||
case PROTECTION_CLASS_D: self = .none
|
||||
#if !os(macOS)
|
||||
case PROTECTION_CLASS_CX: self = .completeWhenUserInactive
|
||||
#endif
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#if !NO_FILESYSTEM
|
||||
// Relocated from FileManager+Files.swift. Originally fileprivate.
|
||||
extension FileAttributeKey {
|
||||
internal static var _extendedAttributes: Self { Self("NSFileExtendedAttributes") }
|
||||
}
|
||||
#endif
|
||||
|
||||
// END: Workaround for https://github.com/swiftlang/swift/pull/78697
|
||||
|
@ -1986,36 +1986,6 @@ extension DataTests {
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA= =", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA== ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: " AQIDBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "A QIDBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQ IDBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQI DBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQID BA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDB A==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA ==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA= =", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA== ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: " AQIDBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "A QIDBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQ IDBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQI DBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQID BA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDB A==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA ==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA= =", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA== ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: " AQIDBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "A QIDBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQ IDBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQI DBA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQID BA==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDB A==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA ==", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA= =", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4]), Data(base64Encoded: "AQIDBA== ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: " AQIDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "A QIDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQ IDBAU=", options: .ignoreUnknownCharacters))
|
||||
@ -2026,36 +1996,6 @@ extension DataTests {
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBAU =", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBAU= ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: " AQIDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "A QIDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQ IDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQI DBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQID BAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDB AU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBA U=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBAU =", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBAU= ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: " AQIDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "A QIDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQ IDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQI DBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQID BAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDB AU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBA U=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBAU =", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBAU= ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: " AQIDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "A QIDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQ IDBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQI DBAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQID BAU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDB AU=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBA U=", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBAU =", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5]), Data(base64Encoded: "AQIDBAU= ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: " AQIDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "A QIDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQ IDBAUG", options: .ignoreUnknownCharacters))
|
||||
@ -2065,36 +2005,6 @@ extension DataTests {
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBA UG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBAU G", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBAUG ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: " AQIDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "A QIDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQ IDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQI DBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQID BAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDB AUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBA UG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBAU G", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBAUG ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: " AQIDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "A QIDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQ IDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQI DBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQID BAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDB AUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBA UG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBAU G", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBAUG ", options: .ignoreUnknownCharacters))
|
||||
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: " AQIDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "A QIDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQ IDBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQI DBAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQID BAUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDB AUG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBA UG", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBAU G", options: .ignoreUnknownCharacters))
|
||||
XCTAssertEqual(Data([1, 2, 3, 4, 5, 6]), Data(base64Encoded: "AQIDBAUG ", options: .ignoreUnknownCharacters))
|
||||
}
|
||||
|
||||
func test_base64Decode_test1MBDataGoing0to255OverAndOver() {
|
||||
|
@ -0,0 +1,138 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2017 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
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if canImport(TestSupport)
|
||||
import TestSupport
|
||||
#endif
|
||||
|
||||
#if canImport(FoundationEssentials)
|
||||
@testable import FoundationEssentials
|
||||
#endif
|
||||
|
||||
#if FOUNDATION_FRAMEWORK
|
||||
@testable import Foundation
|
||||
#endif
|
||||
|
||||
final class HTTPFormatStyleFormattingTests: XCTestCase {
|
||||
|
||||
func test_HTTPFormat() throws {
|
||||
let date = Date.now
|
||||
|
||||
let formatted = date.formatted(.http) // e.g. "Fri, 17 Jan 2025 19:03:05 GMT"
|
||||
let parsed = try? Date(formatted, strategy: .http)
|
||||
|
||||
let result = try XCTUnwrap(parsed)
|
||||
XCTAssertEqual(date.timeIntervalSinceReferenceDate, result.timeIntervalSinceReferenceDate, accuracy: 1.0)
|
||||
}
|
||||
|
||||
func test_HTTPFormat_components() throws {
|
||||
let date = Date.now
|
||||
let formatted = date.formatted(.http) // e.g. "Fri, 17 Jan 2025 19:03:05 GMT"
|
||||
|
||||
let parsed = try? DateComponents(formatted, strategy: .http)
|
||||
|
||||
let result = try XCTUnwrap(parsed)
|
||||
|
||||
let resultDate = Calendar(identifier: .gregorian).date(from: result)
|
||||
let resultDateUnwrapped = try XCTUnwrap(resultDate)
|
||||
|
||||
XCTAssertEqual(date.timeIntervalSinceReferenceDate, resultDateUnwrapped.timeIntervalSinceReferenceDate, accuracy: 1.0)
|
||||
}
|
||||
|
||||
func test_HTTPFormat_variousInputs() throws {
|
||||
let tests = [
|
||||
"Mon, 20 Jan 2025 01:02:03 GMT",
|
||||
"Tue, 20 Jan 2025 10:02:03 GMT",
|
||||
"Wed, 20 Jan 2025 23:02:03 GMT",
|
||||
"Thu, 20 Jan 2025 01:10:03 GMT",
|
||||
"Fri, 20 Jan 2025 01:50:59 GMT",
|
||||
"Sat, 20 Jan 2025 01:50:60 GMT", // 60 is valid, treated as 0
|
||||
"Sun, 20 Jan 2025 01:03:03 GMT",
|
||||
"20 Jan 2025 01:02:03 GMT", // Missing weekdays is ok
|
||||
"20 Jan 2025 10:02:03 GMT",
|
||||
"20 Jan 2025 23:02:03 GMT",
|
||||
"20 Jan 2025 01:10:03 GMT",
|
||||
"20 Jan 2025 01:50:59 GMT",
|
||||
"20 Jan 2025 01:50:60 GMT",
|
||||
"20 Jan 2025 01:03:03 GMT",
|
||||
"Mon, 20 Jan 2025 01:03:03 GMT",
|
||||
"Mon, 03 Feb 2025 01:03:03 GMT",
|
||||
"Mon, 03 Mar 2025 01:03:03 GMT",
|
||||
"Mon, 14 Apr 2025 01:03:03 GMT",
|
||||
"Mon, 05 May 2025 01:03:03 GMT",
|
||||
"Mon, 21 Jul 2025 01:03:03 GMT",
|
||||
"Mon, 04 Aug 2025 01:03:03 GMT",
|
||||
"Mon, 22 Sep 2025 01:03:03 GMT",
|
||||
"Mon, 30 Oct 2025 01:03:03 GMT",
|
||||
"Mon, 24 Nov 2025 01:03:03 GMT",
|
||||
"Mon, 22 Dec 2025 01:03:03 GMT",
|
||||
"Tue, 29 Feb 2028 01:03:03 GMT", // leap day
|
||||
]
|
||||
|
||||
for good in tests {
|
||||
XCTAssertNotNil(try? Date(good, strategy: .http), "Input \(good) was nil")
|
||||
XCTAssertNotNil(try? DateComponents(good, strategy: .http), "Input \(good) was nil")
|
||||
}
|
||||
}
|
||||
|
||||
func test_HTTPFormat_badInputs() throws {
|
||||
let tests = [
|
||||
"Xri, 17 Jan 2025 19:03:05 GMT",
|
||||
"Fri, 17 Janu 2025 19:03:05 GMT",
|
||||
"Fri, 17Jan 2025 19:03:05 GMT",
|
||||
"Fri, 17 Xrz 2025 19:03:05 GMT",
|
||||
"Fri, 17 Jan 2025 19:03:05", // missing GMT
|
||||
"Fri, 1 Jan 2025 19:03:05 GMT",
|
||||
"Fri, 17 Jan 2025 1:03:05 GMT",
|
||||
"Fri, 17 Jan 2025 19:3:05 GMT",
|
||||
"Fri, 17 Jan 2025 19:03:5 GMT",
|
||||
"Fri, 17 Jan 2025 19:03:05 GmT",
|
||||
"Fri, 17 Jan 20252 19:03:05 GMT",
|
||||
"Fri, 17 Jan 252 19:03:05 GMT",
|
||||
"fri, 17 Jan 2025 19:03:05 GMT", // miscapitalized
|
||||
"Fri, 17 jan 2025 19:03:05 GMT",
|
||||
"Fri, 16 Jan 2025 25:03:05 GMT", // nonsense date
|
||||
"Fri, 30 Feb 2025 25:03:05 GMT", // nonsense date
|
||||
]
|
||||
|
||||
for bad in tests {
|
||||
XCTAssertNil(try? Date(bad, strategy: .http), "Input \(bad) was not nil")
|
||||
XCTAssertNil(try? DateComponents(bad, strategy: .http), "Input \(bad) was not nil")
|
||||
}
|
||||
}
|
||||
|
||||
func test_HTTPComponentsFormat() throws {
|
||||
let input = "Fri, 17 Jan 2025 19:03:05 GMT"
|
||||
let parsed = try? DateComponents(input, strategy: .http)
|
||||
|
||||
XCTAssertEqual(parsed?.weekday, 6)
|
||||
XCTAssertEqual(parsed?.day, 17)
|
||||
XCTAssertEqual(parsed?.month, 1)
|
||||
XCTAssertEqual(parsed?.year, 2025)
|
||||
XCTAssertEqual(parsed?.hour, 19)
|
||||
XCTAssertEqual(parsed?.minute, 3)
|
||||
XCTAssertEqual(parsed?.second, 5)
|
||||
XCTAssertEqual(parsed?.timeZone, TimeZone.gmt)
|
||||
}
|
||||
|
||||
func test_validatingResultOfParseVsString() throws {
|
||||
// This date will parse correctly, but of course the value of 99 does not correspond to the actual day.
|
||||
let strangeDate = "Mon, 99 Jan 2025 19:03:05 GMT"
|
||||
let date = try XCTUnwrap(Date(strangeDate, strategy: .http))
|
||||
let components = try XCTUnwrap(DateComponents(strangeDate, strategy: .http))
|
||||
|
||||
let actualDay = Calendar(identifier: .gregorian).component(.day, from: date)
|
||||
let componentDay = try XCTUnwrap(components.day)
|
||||
XCTAssertNotEqual(actualDay, componentDay)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user