Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add rawFractionalSeconds property to GeneralizedTime #53

Merged
merged 4 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion Sources/SwiftASN1/Basic ASN1 Types/GeneralizedTime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@ public struct GeneralizedTime: DERImplicitlyTaggable, BERImplicitlyTaggable, Has
}
set {
self._fractionalSeconds = newValue
self._rawFractionalSeconds = ArraySlice<UInt8>()
try! self._rawFractionalSeconds.append(fractionalSeconds: self._fractionalSeconds)

try! self._validate()
}
}

/// The ArraySlice of bytes from which the fractional seconds will be computed. (Preserved due to a possible overflow
/// when computing a Double from this ArraySlice.)
@inlinable
public var rawFractionalSeconds: ArraySlice<UInt8> {
Lukasa marked this conversation as resolved.
Show resolved Hide resolved
get {
return self._rawFractionalSeconds
}
set {
self._rawFractionalSeconds = newValue
self._fractionalSeconds = try! Double(fromRawFractionalSeconds: self._rawFractionalSeconds)

try! self._validate()
}
}
Expand All @@ -115,7 +133,10 @@ public struct GeneralizedTime: DERImplicitlyTaggable, BERImplicitlyTaggable, Has
@usableFromInline var _hours: Int
@usableFromInline var _minutes: Int
@usableFromInline var _seconds: Int
/// `_fractionalSeconds` is a cached value and `_rawFractionalSeconds` is the source of truth for the numerical
/// fractonal seconds. (No information is lost in the conversion from `_fractionalSeconds` to `_rawFractionalSeconds`.)
@usableFromInline var _fractionalSeconds: Double
Lukasa marked this conversation as resolved.
Show resolved Hide resolved
@usableFromInline var _rawFractionalSeconds: ArraySlice<UInt8>

/// Construct a new ``GeneralizedTime`` from individual components.
///
Expand Down Expand Up @@ -144,6 +165,41 @@ public struct GeneralizedTime: DERImplicitlyTaggable, BERImplicitlyTaggable, Has
self._minutes = minutes
self._seconds = seconds
self._fractionalSeconds = fractionalSeconds
self._rawFractionalSeconds = ArraySlice<UInt8>()
try self._rawFractionalSeconds.append(fractionalSeconds: self._fractionalSeconds)

try self._validate()
}

/// Construct a new ``GeneralizedTime`` from individual components.
///
/// - parameters:
/// - year: The numerical year
/// - month: The numerical month
/// - day: The numerical day
/// - hours: The numerical hours
/// - minutes: The numerical minutes
/// - seconds: The numerical seconds
/// - rawFractionalSeconds: The ArraySlice of bytes from which the fractional seconds will be computed.
/// (Preserved due to a possible overflow when computing a Double from this ArraySlice.)
@inlinable
public init(
year: Int,
month: Int,
day: Int,
hours: Int,
minutes: Int,
seconds: Int,
rawFractionalSeconds: ArraySlice<UInt8>
) throws {
self._year = year
self._month = month
self._day = day
self._hours = hours
self._minutes = minutes
self._seconds = seconds
self._rawFractionalSeconds = rawFractionalSeconds
self._fractionalSeconds = try Double(fromRawFractionalSeconds: self._rawFractionalSeconds)

try self._validate()
}
Expand Down Expand Up @@ -216,6 +272,11 @@ public struct GeneralizedTime: DERImplicitlyTaggable, BERImplicitlyTaggable, Has
reason: "Invalid fractional seconds for GeneralizedTime \(self._fractionalSeconds)"
)
}

// When `rawFractionalSeconds` is converted to a `Double`, it must be equal to `fractionalSeconds`.
assert(
(try? Double(fromRawFractionalSeconds: self._rawFractionalSeconds)) == self._fractionalSeconds
)
}
}

Expand All @@ -228,6 +289,20 @@ extension GeneralizedTime: Comparable {
if lhs.hours < rhs.hours { return true } else if lhs.hours > rhs.hours { return false }
if lhs.minutes < rhs.minutes { return true } else if lhs.minutes > rhs.minutes { return false }
if lhs.seconds < rhs.seconds { return true } else if lhs.seconds > rhs.seconds { return false }
return lhs.fractionalSeconds < rhs.fractionalSeconds
if lhs.fractionalSeconds < rhs.fractionalSeconds {
return true
} else if lhs.fractionalSeconds > rhs.fractionalSeconds {
return false
}

for (lhsByte, rhsByte) in zip(lhs.rawFractionalSeconds, rhs.rawFractionalSeconds) {
if lhsByte != rhsByte {
return lhsByte < rhsByte
}
}

// Since the above `zip` iteration stops at the length of the shorter `Sequence`, finally,
// compare the length of the two `Sequence`s.
return lhs.rawFractionalSeconds.count < rhs.rawFractionalSeconds.count
}
}
102 changes: 62 additions & 40 deletions Sources/SwiftASN1/Basic ASN1 Types/TimeUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ enum TimeUtilities {
}

// There may be some fractional seconds.
var fractionalSeconds: Double = 0
var rawFractionalSeconds = ArraySlice<UInt8>()
if bytes.first == UInt8(ascii: ".") {
fractionalSeconds = try bytes._readFractionalSeconds()
bytes.removeFirst()
rawFractionalSeconds = try bytes._readRawFractionalSeconds()
}

// The next character _must_ be Z, or the encoding is invalid.
Expand All @@ -60,7 +61,7 @@ enum TimeUtilities {
hours: rawHour,
minutes: rawMinutes,
seconds: rawSeconds,
fractionalSeconds: fractionalSeconds
rawFractionalSeconds: rawFractionalSeconds
)
}

Expand Down Expand Up @@ -178,41 +179,44 @@ extension ArraySlice where Element == UInt8 {
return (first &* 10) &+ (second)
}

/// This may only be called if there's a leading period: we precondition on this fact.
@inlinable
mutating func _readFractionalSeconds() throws -> Double {
precondition(self.popFirst() == UInt8(ascii: "."))

var numerator = 0
var denominator = 1
mutating func _readRawFractionalSeconds() throws -> ArraySlice<UInt8> {
guard let nonDecimalASCIIIndex = self.firstIndex(where: { Int(fromDecimalASCII: $0) == nil }) else {
throw ASN1Error.invalidASN1Object(
reason: "Invalid fractional seconds"
)
}

while let nextASCII = self.first, let next = Int(fromDecimalASCII: nextASCII) {
self = self.dropFirst()
// If `nonDecimalASCIIIndex == self.startIndex`, then it means that there is a decimal point
// but there are no fractional seconds
if nonDecimalASCIIIndex == self.startIndex {
throw ASN1Error.invalidASN1Object(
reason: "Invalid fractional seconds"
)
}

let (newNumerator, multiplyOverflow) = numerator.multipliedReportingOverflow(by: 10)
let (newDenominator, secondMultiplyOverflow) = denominator.multipliedReportingOverflow(by: 10)
let (newNumeratorWithAdded, addingOverflow) = newNumerator.addingReportingOverflow(next)
let rawFractionalSeconds = self[..<nonDecimalASCIIIndex]
self = self[nonDecimalASCIIIndex...]
return rawFractionalSeconds
}

// If the new denominator overflows, we just cap to the old value.
if !secondMultiplyOverflow {
denominator = newDenominator
}
@inlinable
mutating func append(fractionalSeconds: Double) throws {
// Fractional seconds may not be negative and may not be 1 or more.
guard fractionalSeconds >= 0 && fractionalSeconds < 1 else {
throw ASN1Error.invalidASN1Object(
reason: "Invalid fractional seconds: \(fractionalSeconds)"
)
}

// If the numerator overflows, we don't support the result.
if multiplyOverflow || addingOverflow {
throw ASN1Error.invalidASN1Object(reason: "Numerator overflow when calculating fractional seconds")
}
if fractionalSeconds != 0 {
let fractionalSecondsAsString = String(fractionalSeconds)

numerator = newNumeratorWithAdded
}
assert(fractionalSecondsAsString.starts(with: "0."), "Invalid fractional seconds")
assert(fractionalSecondsAsString.last != "0", "Trailing zeros in fractional seconds")

// Ok, we're either at the end or the next character is a Z. One final check: there may not have
// been any trailing zeros here. This means the number may not be 0 mod 10.
if numerator % 10 == 0 {
throw ASN1Error.invalidASN1Object(reason: "Trailing zeros in fractional seconds")
self.append(contentsOf: fractionalSecondsAsString.utf8.dropFirst(2))
}

return Double(numerator) / Double(denominator)
}
}

Expand All @@ -226,16 +230,9 @@ extension Array where Element == UInt8 {
self._appendTwoDigitDecimal(generalizedTime.minutes)
self._appendTwoDigitDecimal(generalizedTime.seconds)

// Ok, tricky moment here. Is the fractional part non-zero? If it is, we need to write it out as well.
if generalizedTime.fractionalSeconds != 0 {
let stringified = String(generalizedTime.fractionalSeconds)
assert(stringified.starts(with: "0."))

self.append(contentsOf: stringified.utf8.dropFirst(1))
// Remove any trailing zeros from self, they are forbidden.
while self.last == 0 {
self = self.dropLast()
}
if generalizedTime.rawFractionalSeconds.count > 0 {
self.append(UInt8(ascii: "."))
self.append(contentsOf: generalizedTime.rawFractionalSeconds)
}

self.append(UInt8(ascii: "Z"))
Expand Down Expand Up @@ -300,3 +297,28 @@ extension Int {
self = converted
}
}

extension Double {
@inlinable
init(fromRawFractionalSeconds rawFractionalSeconds: ArraySlice<UInt8>) throws {
if rawFractionalSeconds.count == 0 {
self = 0
return
}

if rawFractionalSeconds.last == UInt8(ascii: "0") {
throw ASN1Error.invalidASN1Object(reason: "Trailing zeros in raw fractional seconds")
}

let rawFractionalSecondsAsString = String(decoding: rawFractionalSeconds, as: UTF8.self)
let fractionalSecondsAsString = "0.\(rawFractionalSecondsAsString)"

guard let fractionalSeconds = Double(fractionalSecondsAsString) else {
throw ASN1Error.invalidASN1Object(
reason: "Invalid raw fractional seconds"
)
}

self = fractionalSeconds
}
}
Loading