Skip to content

Commit

Permalink
make Base16 unsafe API slightly safer, add documentation (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
tayloraswift authored Nov 12, 2022
1 parent 3783866 commit d213ae0
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 45 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">

***`hash`***<br>`0.2.3`
***`hash`***<br>`0.3.0`

[![ci status](https://github.com/kelvin13/swift-hash/actions/workflows/build.yml/badge.svg)](https://github.com/kelvin13/swift-hash/actions/workflows/build.yml)
[![ci status](https://github.com/kelvin13/swift-hash/actions/workflows/build-devices.yml/badge.svg)](https://github.com/kelvin13/swift-hash/actions/workflows/build-devices.yml)
Expand All @@ -12,4 +12,24 @@

</div>

*`swift-hash`* is an inline-only microframework providing generic, pure-Swift implementations of the [SHA-2](https://en.wikipedia.org/wiki/SHA-2) and HMAC-SHA-2 hashing functions.
*`swift-hash`* is an inline-only microframework providing generic, pure-Swift implementations of various hashes, checksums, and binary utilities.

## products

The package vends the following library products:

1. [`Base16`](Sources/Base16)

Tools for encoding to and decoding from base-16 strings.

1. [`Base64`](Sources/Base64)

Tools for encoding to and decoding from base-64 strings.

1. [`CRC`](Sources/CRC)

Implements [CRC-32](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) checksums.

1. [`SHA2`](Sources/SHA2)

Implements the [SHA-256](https://en.wikipedia.org/wiki/SHA-2) and HMAC-SHA-256 hashing functions.
97 changes: 60 additions & 37 deletions Sources/Base16/Base16.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
import BaseDigits

/// A namespace for base-16 utilities.
public
enum Base16
enum Base16
{
/// Decodes some ``String``-like type containing an ASCII-encoded base-16 string
/// to some ``RangeReplaceableCollection`` type. The order of the decoded bytes
/// in the output matches the order of the (pairs of) hexadecimal digits in the
/// input string.
///
/// Characters (including UTF-8 continuation bytes) that are not base-16 digits
/// will be interpreted as zeros. If the string does not contain an even number
/// of digits, the trailing digit will be ignored.
@inlinable public static
func decode<ASCII, Bytes>(_ ascii:ASCII, to _:Bytes.Type = Bytes.self) -> Bytes
where Bytes:RangeReplaceableCollection, Bytes.Element == UInt8,
ASCII:StringProtocol
{
self.decode(ascii.utf8, to: Bytes.self)
}
/// Decodes an ASCII-encoded base-16 string to some ``RangeReplaceableCollection`` type.
/// The order of the decoded bytes in the output matches the order of the (pairs of)
/// hexadecimal digits in the input.
///
/// Characters (including UTF-8 continuation bytes) that are not base-16 digits
/// will be interpreted as zeros. If the input does not yield an even number of
/// digits, the trailing digit will be ignored.
@inlinable public static
func decode<ASCII, Bytes>(_ ascii:ASCII, to _:Bytes.Type = Bytes.self) -> Bytes
where Bytes:RangeReplaceableCollection, Bytes.Element == UInt8,
Expand All @@ -25,7 +41,7 @@ enum Base16
}
return bytes
}

/// Encodes a sequence of bytes to a base-16 string with the specified lettercasing.
@inlinable public static
func encode<Bytes, Digits>(_ bytes:Bytes, with _:Digits.Type) -> String
where Bytes:Sequence, Bytes.Element == UInt8, Digits:BaseDigits
Expand All @@ -42,6 +58,12 @@ enum Base16
}
extension Base16
{
/// Decodes an ASCII-encoded base-16 string into a pre-allocated buffer,
/// returning [`nil`]() if the input did not yield enough bytes to fill
/// the buffer completely.
///
/// Characters (including UTF-8 continuation bytes) that are not base-16 digits
/// will be interpreted as zeros.
@inlinable public static
func decode<ASCII>(_ ascii:ASCII,
into bytes:UnsafeMutableRawBufferPointer) -> Void?
Expand All @@ -62,6 +84,13 @@ extension Base16
}
return ()
}
/// Encodes a sequence of bytes into a pre-allocated buffer as a base-16
/// string with the specified lettercasing.
///
/// The size of the `ascii` buffer must be exactly twice the inline size
/// of `words`. If this method is used incorrectly, the output buffer may
/// be incompletely initialized, but it will never write to memory outside
/// of the buffer’s bounds.
@inlinable public static
func encode<BigEndian, Digits>(storing words:BigEndian,
into ascii:UnsafeMutableRawBufferPointer,
Expand All @@ -72,21 +101,22 @@ extension Base16
{
assert(2 * $0.count <= ascii.count)

var offset:Int = ascii.startIndex
for byte:UInt8 in $0
for (offset, byte):(Int, UInt8)
in zip(stride(from: ascii.startIndex, to: ascii.endIndex, by: 2), $0)
{
ascii[offset] = Digits[byte >> 4]
ascii.formIndex(after: &offset)
ascii[offset] = Digits[byte & 0x0f]
ascii.formIndex(after: &offset)
ascii[offset ] = Digits[byte >> 4]
ascii[offset + 1] = Digits[byte & 0x0f]
}
}
}
}
extension Base16
{
{
#if swift(>=5.6)
@inlinable public static
/// Decodes an ASCII-encoded base-16 string to some (usually trivial) type.
/// This is essentially the same as loading values from raw memory, so this
/// method should only be used to load trivial types.
@inlinable public static
func decode<ASCII, BigEndian>(_ ascii:ASCII,
loading _:BigEndian.Type = BigEndian.self) -> BigEndian?
where ASCII:Sequence, ASCII.Element == UInt8
Expand Down Expand Up @@ -116,13 +146,19 @@ extension Base16
}
#endif


/// Encodes the raw bytes of the given value to a base-16 string with the
/// specified lettercasing. The bytes with the lowest addresses appear first
/// in the encoded output.
///
/// This method is slightly faster than calling ``encode(_:with:)`` on an
/// unsafe buffer-pointer view of `words`.
@inlinable public static
func encode<BigEndian, Digits>(storing words:BigEndian,
with _:Digits.Type) -> String
where Digits:BaseDigits
{
let bytes:Int = 2 * MemoryLayout<BigEndian>.size

#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 14.0, *)
{
Expand All @@ -134,39 +170,26 @@ extension Base16
return bytes
}
}
else
{
return .init(
decoding: try Self.encode(storing: words, to: [UInt8].self, with: Digits.self),
as: Unicode.UTF8.self)
}
#elseif swift(>=5.4)
return .init(unsafeUninitializedCapacity: bytes)
{
Self.encode(storing: words,
into: UnsafeMutableRawBufferPointer.init($0),
with: Digits.self)
return bytes
}
#else
#endif

#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || swift(<5.4)
return .init(
decoding: try Self.encode(storing: words, to: [UInt8].self, with: Digits.self),
decoding: [UInt8].init(unsafeUninitializedCapacity: bytes)
{
Self.encode(storing: words,
into: UnsafeMutableRawBufferPointer.init($0),
with: Digits.self)
$1 = bytes
},
as: Unicode.UTF8.self)
#endif
}

@inlinable public static
func encode<BigEndian, Digits>(storing words:BigEndian, to _:[UInt8].Type,
with _:Digits.Type) -> [UInt8]
where Digits:BaseDigits
{
let bytes:Int = 2 * MemoryLayout<BigEndian>.size
#else
return .init(unsafeUninitializedCapacity: bytes)
{
Self.encode(storing: words,
into: UnsafeMutableRawBufferPointer.init($0),
with: Digits.self)
$1 = bytes
return bytes
}
#endif
}
}
16 changes: 10 additions & 6 deletions Sources/Base64/Base64.Input.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
extension Base64
{
/// An abstraction over text input, which discards ASCII whitespace
/// characters.
/// An abstraction over text input, which discards the ASCII whitespace
/// characters [`'\t'`](), [`'\n'`](), [`'\f'`](), [`'\r'`](), and [`' '`]().
///
/// Iteration over an instance of this type will halt upon encountering the
/// first [`'='`]() padding character, even if the underlying sequence contains
/// more characters.
@frozen public
struct Input<UTF8> where UTF8:Sequence, UTF8.Element == UInt8
struct Input<ASCII> where ASCII:Sequence, ASCII.Element == UInt8
{
public
var iterator:UTF8.Iterator
var iterator:ASCII.Iterator

@inlinable public
init(_ utf8:UTF8)
init(_ ascii:ASCII)
{
self.iterator = utf8.makeIterator()
self.iterator = ascii.makeIterator()
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions Sources/Base64/Base64.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
import BaseDigits

/// A namespace for base-64 utilities.
///
/// The interface is superficially similar to that of the ``/Base16`` module,
/// but the decoding methods are slightly more lenient in their inputs, as
/// they ignore whitespace and newlines.
public
enum Base64
{
/// Decodes some ``String``-like type containing an ASCII-encoded base-64 string
/// to some ``RangeReplaceableCollection`` type, skipping over any ASCII
/// whitespace characters. Padding is not required.
///
/// Characters (including UTF-8 continuation bytes) that are neither base-64 digits
/// nor ASCII whitespace characters will be interpreted as zeros.
///
/// See ``Base64/Input`` for a list of recognized ASCII whitespace characters.
///
/// > Important:
/// Unicode whitespace characters, such as non-breaking spaces, will *not*
/// be skipped, and their constituent UTF-8 code units will be interpreted
/// as zeros.
@inlinable public static
func decode<ASCII, Bytes>(_ ascii:ASCII, to _:Bytes.Type = Bytes.self) -> Bytes
where Bytes:RangeReplaceableCollection, Bytes.Element == UInt8,
ASCII:StringProtocol
{
self.decode(ascii.utf8, to: Bytes.self)
}
/// Decodes an ASCII-encoded base-64 string to some ``RangeReplaceableCollection`` type,
/// skipping over any ASCII whitespace characters. Padding is not required.
///
/// Characters (including UTF-8 continuation bytes) that are neither base-64 digits
/// nor ASCII whitespace characters will be interpreted as zeros.
///
/// See ``Base64/Input`` for a list of recognized ASCII whitespace characters.
///
/// > Important:
/// Unicode whitespace characters, such as non-breaking spaces, will *not*
/// be skipped, and their constituent UTF-8 code units will be interpreted
/// as zeros.
@inlinable public static
func decode<ASCII, Bytes>(_ ascii:ASCII, to _:Bytes.Type = Bytes.self) -> Bytes
where Bytes:RangeReplaceableCollection, Bytes.Element == UInt8,
Expand Down Expand Up @@ -43,6 +73,7 @@ enum Base64
return bytes
}

/// Encodes a sequence of bytes to a base-64 string with padding if needed.
@inlinable public static
func encode<Bytes>(_ bytes:Bytes) -> String where Bytes:Sequence, Bytes.Element == UInt8
{
Expand Down
2 changes: 2 additions & 0 deletions Sources/BaseDigits/BaseDigits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ protocol BaseDigits
}
extension BaseDigits
{
/// Gets the ASCII value for the given remainder as a ``Unicode/Scalar``.
@inlinable public static
subscript(remainder:UInt8, as _:Unicode.Scalar.Type = Unicode.Scalar.self) -> Unicode.Scalar
{
.init(Self[remainder])
}
/// Gets the ASCII value for the given remainder as a ``Character``.
@inlinable public static
subscript(remainder:UInt8, as _:Character.Type = Character.self) -> Character
{
Expand Down

0 comments on commit d213ae0

Please sign in to comment.