Swift 6.4 migration and exhancements (#1)
This PR contains the work done to update the library and the attached example project to use the Swift 6.4 computer as a minimum supported version and also, to use the latest features introduced in it. Reviewed-on: #1 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
internal import CPlaydate
|
||||
|
||||
extension File {
|
||||
/// An open file. Wraps `SDFile`. The file is closed on deinit if it has
|
||||
/// not been closed explicitly.
|
||||
public final class Handle {
|
||||
/// An open file. Wraps `SDFile`. Non-copyable: closes when the handle goes out of
|
||||
/// scope, or earlier via consuming `close()`. At most 64 files may be open.
|
||||
public struct Handle: ~Copyable {
|
||||
let pointer: UnsafeMutableRawPointer
|
||||
private var isClosed = false
|
||||
|
||||
/// Opens the file at `path`.
|
||||
/// Opens the file at `path` in `mode`.
|
||||
public init(path: String, mode: Options) throws(PlaydateError) {
|
||||
let pointer = path.withPlaydateCString {
|
||||
let pointer = path.withCString {
|
||||
fileAPI.pointee.open.unsafelyUnwrapped($0, mode.cValue)
|
||||
}
|
||||
guard let pointer else { throw lastFileError() }
|
||||
@@ -17,68 +16,69 @@ extension File {
|
||||
}
|
||||
|
||||
deinit {
|
||||
if !isClosed {
|
||||
_ = fileAPI.pointee.close.unsafelyUnwrapped(pointer)
|
||||
}
|
||||
_ = fileAPI.pointee.close.unsafelyUnwrapped(pointer)
|
||||
}
|
||||
|
||||
/// Closes the file. Further operations are invalid.
|
||||
public func close() throws(PlaydateError) {
|
||||
guard !isClosed else { return }
|
||||
isClosed = true
|
||||
/// Closes the file, consuming the handle.
|
||||
// `@export(interface)` lets `discard` compile in Embedded Swift on the 6.4 toolchain.
|
||||
@export(interface)
|
||||
public consuming func close() throws(PlaydateError) {
|
||||
let pointer = self.pointer
|
||||
discard self
|
||||
if fileAPI.pointee.close.unsafelyUnwrapped(pointer) != 0 { throw lastFileError() }
|
||||
}
|
||||
|
||||
/// Reads up to `buffer.count` bytes into `buffer`. Returns the number
|
||||
/// of bytes read; 0 indicates end of file.
|
||||
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(PlaydateError) -> Int {
|
||||
let result = fileAPI.pointee.read.unsafelyUnwrapped(
|
||||
pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||
if result < 0 { throw lastFileError() }
|
||||
return Int(result)
|
||||
}
|
||||
|
||||
/// Reads up to `length` bytes and returns them.
|
||||
public func read(length: Int) throws(PlaydateError) -> [UInt8] {
|
||||
var bytes = [UInt8](repeating: 0, count: length)
|
||||
let result = bytes.withUnsafeMutableBytes { buffer in
|
||||
/// Reads up to `buffer.count` bytes; returns the count read, 0 at end of file.
|
||||
public func read(into buffer: inout MutableSpan<UInt8>) throws(PlaydateError) -> Int {
|
||||
let result = buffer.withUnsafeMutableBufferPointer { buffer in
|
||||
fileAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||
}
|
||||
if result < 0 { throw lastFileError() }
|
||||
bytes.removeLast(length - Int(result))
|
||||
return bytes
|
||||
}
|
||||
|
||||
/// Writes the buffer to the file. Returns the number of bytes written.
|
||||
@discardableResult
|
||||
public func write(_ buffer: UnsafeRawBufferPointer) throws(PlaydateError) -> Int {
|
||||
let result = fileAPI.pointee.write.unsafelyUnwrapped(
|
||||
pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||
if result < 0 { throw lastFileError() }
|
||||
return Int(result)
|
||||
}
|
||||
|
||||
/// Writes the bytes to the file. Returns the number of bytes written.
|
||||
/// Reads up to `length` bytes; shorter near end of file, empty at it.
|
||||
public func read(length: Int) throws(PlaydateError) -> [UInt8] {
|
||||
try [UInt8](capacity: length) { output throws(PlaydateError) in
|
||||
let result = output.withUnsafeMutableBufferPointer { buffer, initializedCount in
|
||||
let result = fileAPI.pointee.read.unsafelyUnwrapped(
|
||||
pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||
initializedCount = max(Int(result), 0)
|
||||
return result
|
||||
}
|
||||
if result < 0 { throw lastFileError() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes `bytes`; returns the count written.
|
||||
@discardableResult
|
||||
public func write(_ bytes: [UInt8]) throws(PlaydateError) -> Int {
|
||||
let result = bytes.withUnsafeBytes { buffer in
|
||||
public func write(_ bytes: Span<UInt8>) throws(PlaydateError) -> Int {
|
||||
let result = bytes.withUnsafeBufferPointer { buffer in
|
||||
fileAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||
}
|
||||
if result < 0 { throw lastFileError() }
|
||||
return Int(result)
|
||||
}
|
||||
|
||||
/// Writes the string's UTF-8 to the file. Returns the bytes written.
|
||||
/// Writes `bytes`; returns the count written.
|
||||
@discardableResult
|
||||
public func write(_ bytes: [UInt8]) throws(PlaydateError) -> Int {
|
||||
try bytes.withUnsafeBufferPointer { buffer throws(PlaydateError) in
|
||||
try write(buffer.span)
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes `string` as UTF-8, without a NUL terminator; returns the count written.
|
||||
@discardableResult
|
||||
public func write(_ string: String) throws(PlaydateError) -> Int {
|
||||
let result = string.withPlaydateUTF8 { bytes, count in
|
||||
fileAPI.pointee.write.unsafelyUnwrapped(pointer, bytes, UInt32(count))
|
||||
let result = string.withCString { cString in
|
||||
fileAPI.pointee.write.unsafelyUnwrapped(pointer, cString, UInt32(string.utf8.count))
|
||||
}
|
||||
if result < 0 { throw lastFileError() }
|
||||
return Int(result)
|
||||
}
|
||||
|
||||
/// Flushes buffered writes to disk. Returns the bytes written.
|
||||
/// Flushes buffered writes; returns the count written.
|
||||
@discardableResult
|
||||
public func flush() throws(PlaydateError) -> Int {
|
||||
let result = fileAPI.pointee.flush.unsafelyUnwrapped(pointer)
|
||||
@@ -86,14 +86,14 @@ extension File {
|
||||
return Int(result)
|
||||
}
|
||||
|
||||
/// The current read/write offset.
|
||||
/// The current read/write offset, in bytes.
|
||||
public func tell() throws(PlaydateError) -> Int {
|
||||
let result = fileAPI.pointee.tell.unsafelyUnwrapped(pointer)
|
||||
if result < 0 { throw lastFileError() }
|
||||
return Int(result)
|
||||
}
|
||||
|
||||
/// Moves the read/write offset to `offset` relative to `origin`.
|
||||
/// Moves the read/write offset to `offset` bytes from `origin`.
|
||||
public func seek(to offset: Int, from origin: SeekOrigin = .start) throws(PlaydateError) {
|
||||
if fileAPI.pointee.seek.unsafelyUnwrapped(pointer, Int32(offset), origin.rawValue) != 0 {
|
||||
throw lastFileError()
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
extension File {
|
||||
/// The origin used by `Handle.seek(to:from:)`.
|
||||
/// Origin for `Handle.seek(to:from:)`: `SEEK_SET`, `SEEK_CUR`, `SEEK_END`.
|
||||
public enum SeekOrigin: Int32, Sendable {
|
||||
/// Relative to the beginning of the file.
|
||||
case start = 0
|
||||
/// Relative to the current offset.
|
||||
case current = 1
|
||||
/// Relative to the end of the file.
|
||||
case end = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
internal import CPlaydate
|
||||
|
||||
/// The cached `playdate->file` C API table.
|
||||
/// Cached `playdate->file` table.
|
||||
var fileAPI: UnsafePointer<playdate_file> { Playdate.fileAPI.unsafelyUnwrapped }
|
||||
|
||||
/// The most recent file system error as a thrown error.
|
||||
/// The most recent file error, with the OS's description.
|
||||
func lastFileError() -> PlaydateError {
|
||||
PlaydateError(cString: fileAPI.pointee.geterr.unsafelyUnwrapped())
|
||||
}
|
||||
|
||||
/// The file API: access to the game's Data directory and pdx contents.
|
||||
///
|
||||
/// Paths are relative to the game's Data directory (read/write) or the
|
||||
/// game's pdx (read-only), depending on the mode used to open them.
|
||||
/// The file API. Paths are relative to the Data directory (writable) or the pdx (read-only).
|
||||
/// Every throwing API throws `PlaydateError` with the OS's description on failure.
|
||||
public enum File {}
|
||||
|
||||
extension File {
|
||||
// MARK: - Directory operations
|
||||
|
||||
/// Calls `each` with the name of every file in `path`. Subdirectory names
|
||||
/// end in a slash. Throws if the directory does not exist.
|
||||
/// Calls `each` with each entry name in `path`, non-recursively; directories end in `/`.
|
||||
/// Skips `.`-prefixed names unless `showHidden`. Throws if `path` can't be opened.
|
||||
public static func listFiles(at path: String, showHidden: Bool = false,
|
||||
_ each: (String) -> Void) throws(PlaydateError) {
|
||||
let result = withoutActuallyEscaping(each) { each in
|
||||
var callback = each
|
||||
return path.withPlaydateCString { cPath in
|
||||
return path.withCString { cPath in
|
||||
withUnsafeMutablePointer(to: &callback) { callbackPointer in
|
||||
fileAPI.pointee.listfiles.unsafelyUnwrapped(cPath, { cName, userdata in
|
||||
guard let cName, let userdata else { return }
|
||||
let each = userdata.assumingMemoryBound(to: ((String) -> Void).self).pointee
|
||||
each(String(playdateCString: cName))
|
||||
each(String(cString: cName))
|
||||
}, callbackPointer, showHidden ? 1 : 0)
|
||||
}
|
||||
}
|
||||
@@ -36,10 +34,10 @@ extension File {
|
||||
if result != 0 { throw lastFileError() }
|
||||
}
|
||||
|
||||
/// Information about the file or directory at `path`.
|
||||
/// Information about the file or directory at `path`; throws if it is missing.
|
||||
public static func stat(_ path: String) throws(PlaydateError) -> Stat {
|
||||
var stat = FileStat()
|
||||
let result = path.withPlaydateCString { fileAPI.pointee.stat.unsafelyUnwrapped($0, &stat) }
|
||||
let result = path.withCString { fileAPI.pointee.stat.unsafelyUnwrapped($0, &stat) }
|
||||
if result != 0 { throw lastFileError() }
|
||||
return Stat(
|
||||
isDirectory: stat.isdir != 0,
|
||||
@@ -49,26 +47,25 @@ extension File {
|
||||
hour: UInt8(stat.m_hour), minute: UInt8(stat.m_minute), second: UInt8(stat.m_second)))
|
||||
}
|
||||
|
||||
/// Creates a directory (and intermediate directories) in the Data directory.
|
||||
/// Creates directory `path` in the Data directory; does not create intermediate ones.
|
||||
public static func mkdir(_ path: String) throws(PlaydateError) {
|
||||
let result = path.withPlaydateCString { fileAPI.pointee.mkdir.unsafelyUnwrapped($0) }
|
||||
let result = path.withCString { fileAPI.pointee.mkdir.unsafelyUnwrapped($0) }
|
||||
if result != 0 { throw lastFileError() }
|
||||
}
|
||||
|
||||
/// Deletes the file or directory at `path`. Directories require
|
||||
/// `recursive` to be deleted with their contents.
|
||||
/// Deletes the file at `path`; with `recursive`, a directory and its contents.
|
||||
public static func unlink(_ path: String, recursive: Bool = false) throws(PlaydateError) {
|
||||
let result = path.withPlaydateCString {
|
||||
let result = path.withCString {
|
||||
fileAPI.pointee.unlink.unsafelyUnwrapped($0, recursive ? 1 : 0)
|
||||
}
|
||||
if result != 0 { throw lastFileError() }
|
||||
}
|
||||
|
||||
/// Renames (moves) a file in the Data directory, overwriting any existing
|
||||
/// file at the destination.
|
||||
/// Moves `from` to `to` in the Data directory, overwriting `to`; does not create
|
||||
/// intermediate directories.
|
||||
public static func rename(from: String, to: String) throws(PlaydateError) {
|
||||
let result = from.withPlaydateCString { cFrom in
|
||||
to.withPlaydateCString { cTo in
|
||||
let result = from.withCString { cFrom in
|
||||
to.withCString { cTo in
|
||||
fileAPI.pointee.rename.unsafelyUnwrapped(cFrom, cTo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
internal import CPlaydate
|
||||
|
||||
extension File {
|
||||
/// How to open a file.
|
||||
/// How to open a file. Wraps `FileOptions`.
|
||||
public struct Options: OptionSet, Sendable {
|
||||
public let rawValue: UInt32
|
||||
public init(rawValue: UInt32) { self.rawValue = rawValue }
|
||||
|
||||
/// Read from the game pdx, then the Data directory.
|
||||
/// Read from the pdx only; add `.readData` to search the Data directory first.
|
||||
public static let read = Options(rawValue: UInt32(kFileRead.rawValue))
|
||||
/// Read from the Data directory only.
|
||||
/// Read from the Data directory.
|
||||
public static let readData = Options(rawValue: UInt32(kFileReadData.rawValue))
|
||||
/// Write to the Data directory, truncating an existing file.
|
||||
/// Write to the Data directory, truncating.
|
||||
public static let write = Options(rawValue: UInt32(kFileWrite.rawValue))
|
||||
/// Write to the Data directory, appending to an existing file.
|
||||
/// Write to the Data directory, appending.
|
||||
public static let append = Options(rawValue: UInt32(kFileAppend.rawValue))
|
||||
|
||||
var cValue: FileOptions { FileOptions(FileOptions.RawValue(rawValue)) }
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
extension File {
|
||||
/// Information about a file or directory, mirroring `FileStat`.
|
||||
/// File or directory information. Mirrors `FileStat`.
|
||||
public struct Stat: Sendable {
|
||||
/// Whether the path is a directory.
|
||||
public let isDirectory: Bool
|
||||
/// The file's size, in bytes.
|
||||
/// Size in bytes.
|
||||
public let size: UInt32
|
||||
/// The time the file was last modified.
|
||||
/// Last modification time; `weekday` is 0 (unset).
|
||||
public let modified: System.DateTime
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user