diff --git a/.gitignore b/.gitignore index 2c22487..5964b45 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,5 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output + +.DS_Store diff --git a/Configurations/Debug.xcconfig b/Configurations/Debug.xcconfig new file mode 100644 index 0000000..c6934ba --- /dev/null +++ b/Configurations/Debug.xcconfig @@ -0,0 +1,4 @@ +#include "Project.xcconfig" + +ONLY_ACTIVE_ARCH = YES +SWIFT_OPTIMIZATION_LEVEL = -Onone diff --git a/Configurations/Project.xcconfig b/Configurations/Project.xcconfig new file mode 100644 index 0000000..5d3cefc --- /dev/null +++ b/Configurations/Project.xcconfig @@ -0,0 +1,5 @@ +SDKROOT = macosx +WARNING_CFLAGS = -Wall -Wextra + +SWIFT_VERSION = 3.0 +SWIFT_INCLUDE_PATHS = "${PROJECT_DIR}/Modules" diff --git a/Configurations/Release.xcconfig b/Configurations/Release.xcconfig new file mode 100644 index 0000000..e48b233 --- /dev/null +++ b/Configurations/Release.xcconfig @@ -0,0 +1,5 @@ +#include "Project.xcconfig" + +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) NDEBUG=1 + +SWIFT_OPTIMIZATION_LEVEL = -O diff --git a/Extras/DictionaryExtras.swift b/Extras/DictionaryExtras.swift new file mode 100644 index 0000000..972c2a4 --- /dev/null +++ b/Extras/DictionaryExtras.swift @@ -0,0 +1,18 @@ +// +// DictionaryExtras.swift +// SwresTools +// + +extension Dictionary { + init(_ elements: Array){ + self.init() + for (key, value) in elements { + self[key] = value + } + } + + func flatMap(transform: (Key, Value) -> (Key, Value)?) -> Dictionary { + return Dictionary(self.flatMap(transform)) + } +} + diff --git a/Extras/ErrorExtras.swift b/Extras/ErrorExtras.swift new file mode 100644 index 0000000..0ee69ff --- /dev/null +++ b/Extras/ErrorExtras.swift @@ -0,0 +1,28 @@ +// +// ErrorExtras.swift +// SwresTools +// + +protocol SwresError: Error, CustomStringConvertible { +} + +protocol NestingSwresError: SwresError { + var underlyingError: Error? { get } +} + +extension Error { + func shortDescription(withUnderlyingError: Bool = false) -> String { + switch self { + case let error as NestingSwresError: + var string = error.description + if let underlyingError = error.underlyingError, withUnderlyingError == true { + string += "\n" + underlyingError.shortDescription(withUnderlyingError: true) + } + return string + case let error as SwresError: + return error.description + default: + return localizedDescription + } + } +} diff --git a/Extras/PointerExtras.swift b/Extras/PointerExtras.swift new file mode 100644 index 0000000..3cd6385 --- /dev/null +++ b/Extras/PointerExtras.swift @@ -0,0 +1,36 @@ +// +// PointerExtras.swift +// SwresTools +// + +import Darwin + +// For performance, there are a few places strings are passed around as a base pointer +// and a count of bytes. The Swift standard library includes `UnsafeBufferPointer` and +// `UnsafeMutableBufferPointer` which could wrap that pointer and length, but it's +// surprisingly slow. Replacing that with this tiny `Buffer` struct sped up string +// formatting hotpaths considerably. +struct Buffer { + let pointer: UnsafePointer + let count: Int +} + +class ManagedUnsafeMutablePointer: Hashable { + let pointer: UnsafeMutablePointer + + init(adoptPointer: UnsafeMutablePointer) { + pointer = adoptPointer + } + + deinit { + free(pointer) + } + + var hashValue: Int { + return pointer.hashValue + } + + static func ==(lhs: ManagedUnsafeMutablePointer, rhs: ManagedUnsafeMutablePointer) -> Bool { + return lhs.pointer == rhs.pointer + } +} diff --git a/Extras/SequenceExtras.swift b/Extras/SequenceExtras.swift new file mode 100644 index 0000000..48a838e --- /dev/null +++ b/Extras/SequenceExtras.swift @@ -0,0 +1,22 @@ +// +// SequenceExtras.swift +// SwresTools +// + +extension Sequence { + func firstSome(_ transform: @escaping (Iterator.Element) -> ElementOfResult?) -> ElementOfResult? { + return self.lazy.flatMap(transform).first + } + + func groupBy(_ transform: (Iterator.Element) -> ElementOfResult) -> Dictionary> { + var groupedBy: Dictionary> = Dictionary() + for item in self { + let transformed = transform(item) + if groupedBy[transformed] == nil { + groupedBy[transformed] = Array() + } + groupedBy[transformed]!.append(item) + } + return groupedBy + } +} diff --git a/Extras/StrideExtras.swift b/Extras/StrideExtras.swift new file mode 100644 index 0000000..a348ae8 --- /dev/null +++ b/Extras/StrideExtras.swift @@ -0,0 +1,10 @@ +// +// StrideExtras.swift +// SwresTools +// + +func offsetAndLengthStride(from: Int, to: Int, by: Int, _ block: (Int, Int) -> Void) { + for offset in stride(from: from, to: to, by: by) { + block(offset, min(by, to - offset)) + } +} diff --git a/Extras/StringExtras.swift b/Extras/StringExtras.swift new file mode 100644 index 0000000..cf3836d --- /dev/null +++ b/Extras/StringExtras.swift @@ -0,0 +1,100 @@ +// +// StringFunctions.swift +// SwresTools +// + +import Foundation + +private let MinimumDisplayableByteValue: UInt8 = 32 + +let MacOSRomanByteFullStop: UInt8 = 0x2E +let MacOSRomanByteQuestionMark: UInt8 = 0x3F + +struct MacOSRomanConversionOptions { + let filterControlCharacters: Bool + let filterFilesystemUnsafeCharacters: Bool + let filterNonASCIICharacters: Bool + let replacementMacOSRomanByte: UInt8? +} + +func stringFromMacOSRomanBytes(_ buffer: Buffer, options: MacOSRomanConversionOptions) -> String { + let filterControlCharacters = options.filterControlCharacters || options.filterFilesystemUnsafeCharacters + + let filteredBytes = UnsafeMutablePointer.allocate(capacity: buffer.count + 1) + defer { + filteredBytes.deallocate(capacity: buffer.count + 1) + } + + var filteredByteIterator = filteredBytes + + @inline(__always) func writeFilteredByte(_ byte: UInt8) { + filteredByteIterator.pointee = byte + filteredByteIterator += 1 + } + + var bufferPointer = buffer.pointer + let endPointer = buffer.pointer + buffer.count + + while bufferPointer < endPointer { + var replaceCharacter = false + let byte = bufferPointer.pointee + + // SPACE, DELETE + if filterControlCharacters, byte < 0x20 || byte == 0x7F { + replaceCharacter = true + } + // DELETE + else if options.filterNonASCIICharacters, byte > 0x7F { + replaceCharacter = true + } + // ASTERISK, FULL STOP, SOLIDUS, COLON, REVERSE SOLIDUS, TILDE + // Some of these aren't technically unsafe, but they can cause issues in the + // shell or Finder, such as a period at the start of a file or a tilde. + else if options.filterFilesystemUnsafeCharacters, byte == 0x2A || byte == 0x2E || byte == 0x2F || byte == 0x3A || byte == 0x5C || byte == 0x7E { + replaceCharacter = true + } + + if replaceCharacter, let unwrappedReplacementByte = options.replacementMacOSRomanByte { + writeFilteredByte(unwrappedReplacementByte) + } else if !replaceCharacter { + writeFilteredByte(byte) + } + + bufferPointer += 1 + } + + filteredByteIterator.pointee = 0 + + let cStringPointer = UnsafeRawPointer(filteredBytes).assumingMemoryBound(to: CChar.self) + return String(cString: cStringPointer, encoding: String.Encoding.macOSRoman)! +} + +func stringFromMacOSRomanBytes(_ data: Data, options: MacOSRomanConversionOptions) -> String { + return data.withUnsafeBytes { (bytes: UnsafePointer) in + let buffer = Buffer(pointer: bytes, count: data.count) + return stringFromMacOSRomanBytes(buffer, options: options) + } +} + +func filesystemSafeString(_ data: Data) -> String { + let options = MacOSRomanConversionOptions(filterControlCharacters: true, filterFilesystemUnsafeCharacters: true, filterNonASCIICharacters: false, replacementMacOSRomanByte: nil) + return stringFromMacOSRomanBytes(data, options: options) +} + +extension String { + func copyCString() -> ManagedUnsafeMutablePointer { + return self.withCString({ (cString: UnsafePointer) -> ManagedUnsafeMutablePointer in + return ManagedUnsafeMutablePointer(adoptPointer: strdup(cString)) + }) + } +} + +struct StringAndCString { + let string: String + let cString: ManagedUnsafeMutablePointer + + init(_ string: String) { + self.string = string + cString = string.copyCString() + } +} diff --git a/Modules/FUSE/module.modulemap b/Modules/FUSE/module.modulemap new file mode 100644 index 0000000..f9f5d93 --- /dev/null +++ b/Modules/FUSE/module.modulemap @@ -0,0 +1,5 @@ +module Fuse [system] { + header "/usr/local/include/osxfuse/fuse.h" + link "osxfuse" + export * +} diff --git a/README b/README new file mode 100644 index 0000000..a3ef517 --- /dev/null +++ b/README @@ -0,0 +1,49 @@ +SwresTools is a set of two tools for exploring and dumping classic Macintosh resource forks, written in Swift. + +SwresTools can convert some resources to modern types. For example, it can translate some snd resources to WAV files. + +To be honest, this was just a fun side project and shouldn't be used for anything important. + +### SwresExplode + +`SwresExplode` dumps the resources of a classic Macintosh resource fork to individual files. + +For example, list all of the resource types in a resource fork: + + SwresExplode resourcefile + +Dump all `snd ` resources to individual files: + + SwresExplode -d -t 'snd ' + +### SwresFUSE + +`SwresFUSE` mounts a resource fork as a filesystem. It requires [FUSE for macOS][fuse]. + +Example usage: + + SwresFUSE resourcefile mountpoint + +![A screenshot of SwresFUSE exploring a resource fork.](SwresFUSE.png) + +[fuse]: https://osxfuse.github.io + +### Why? + +¯\\_(ツ)_/¯ + +### No, Really, Why? + +I wanted to rip the music out of the old Mac game [Troubled Souls][troubledsouls], and this seemed like such a bad way to go about it I had to give it a go. I was playing around with Swift and thought this would be a fun way to write a project that interfaced with a C library. + +[troubledsouls]: https://en.wikipedia.org/wiki/Troubled_Souls + +### Buiding + +You may need to point the header parameter of Modules/FUSE/module.modulemap to your FUSE header directory. For some reason module maps don't seem to respect header search paths and need absolute paths. + +### Limitations + +* The translators are very limited. Only Pascal style prefix strings and an extremely limited subset of `SND` resources are supported. +* ResEdit features like `RMAP` and `TMPL` resources are not supported. +* Editing or creating new resources is not supported. \ No newline at end of file diff --git a/SwresExplode/SwresExplode.swift b/SwresExplode/SwresExplode.swift new file mode 100644 index 0000000..ea0e428 --- /dev/null +++ b/SwresExplode/SwresExplode.swift @@ -0,0 +1,367 @@ +// +// SwresExplode.swift +// SwresTools +// + +import Foundation + +struct ExplodeTask { + var printResources: Bool = false + var dumpResources: Bool = false + var overwriteExistingFiles: Bool = false + var translatorFilter: TranslatorFilter = TranslatorFilter.noTranslators + var identifierFilter: Int16? + var typeFilter: FourCharCode? + var inputURL: URL? + var outputFolder: URL = URL(fileURLWithPath: "SwresExplode") +} + +func printUsageAndExit(status: Int32 = EXIT_SUCCESS) -> Never { + let processName = ProcessInfo.processInfo.processName + + print("Extract Macintosh Toolbox resources.") + print("Usage: \(processName) [options] resourcefile") + print("Options:") + print(" -h Show this help message") + print("Filtering Options:") + print(" -i [id] Filter by resource identifier.") + print(" -t [xxxx] Filter by resource type.") + print("Dumping Options:") + print(" -p Print resources to standard output.") + print(" -d Dump resources to files.") + print(" -o Output directory for the dumped resource.") + print(" -f Overwrite existing files when dumping.") + print(" -c Attempt to convert resources into more modern or portable formats.") + print(" -C Also use best guess conversions.") + print("Examples:") + print(" \(processName) resourcefile List all of the types.") + print(" \(processName) -d -t 'snd ' Dump all `snd ' resources.") + print(" \(processName) -d -t 'snd ' -i 1000 Dump the `snd ' resource with id 1000.") + print(" \(processName) -d -o /tmp/foo Dump all resources to the directory /tmp/foo.") + + exit(status) +} + +func taskForArguments() -> ExplodeTask { + var task = ExplodeTask() + let argc = CommandLine.argc + opterr = 0 + + while true { + let option = getopt(argc, CommandLine.unsafeArgv, "hi:t:pdfcCo:") + if option == -1 { + break + } + + let optionScalar = UnicodeScalar(Int(option))! + switch optionScalar { + case UnicodeScalar("h"): + printUsageAndExit() + case UnicodeScalar("i"): + let identifierNumber = String(cString: optarg) + task.identifierFilter = Int16(identifierNumber) + case UnicodeScalar("t"): + do { + task.typeFilter = try FourCharCode(optarg) + } catch FourCharCodeError.invalidSequence { + print("Invalid type filter."); + printUsageAndExit(status: EXIT_FAILURE) + } catch { + print("Unexpected error parsing type filter.") + printUsageAndExit(status: EXIT_FAILURE) + } + case UnicodeScalar("p"): + task.printResources = true + case UnicodeScalar("d"): + task.dumpResources = true + case UnicodeScalar("o"): + task.outputFolder = URL(fileURLWithPath: String(cString: optarg)) + case UnicodeScalar("f"): + task.overwriteExistingFiles = true + case UnicodeScalar("c"): + task.translatorFilter = max(task.translatorFilter, TranslatorFilter.onlyLikelyTranslators) + case UnicodeScalar("C"): + task.translatorFilter = max(task.translatorFilter, TranslatorFilter.likelyAndPossibleTranslators) + case UnicodeScalar("?"): + let unknownOption = UnicodeScalar(Int(optopt))! + if unknownOption == UnicodeScalar("i") || unknownOption == UnicodeScalar("t") { + print("Option -\(unknownOption) requires an argument.") + } else { + print("Unknown option -\(unknownOption.escaped(asASCII: true)).") + } + printUsageAndExit(status: EXIT_FAILURE) + default: + printUsageAndExit(status: EXIT_FAILURE) + } + } + + guard optind < argc else { + print("No input file specified.") + printUsageAndExit(status: EXIT_FAILURE) + } + + let inputPathBytes = CommandLine.unsafeArgv[Int(optind)]! + let inputPath = String(cString:inputPathBytes) + task.inputURL = URL(fileURLWithPath: inputPath) + + return task +} + +func run(_ task: ExplodeTask) -> Int32 { + let resourcesByType = read(task) + process(task: task, resourcesByType: resourcesByType) + return EXIT_SUCCESS +} + +func read(_ task: ExplodeTask) -> ResourcesByType { + do { + return try readResourceFork(task.inputURL!) + } catch let error { + print(error.shortDescription(withUnderlyingError: true)) + exit(EXIT_FAILURE) + } +} + +func process(task: ExplodeTask, resourcesByType: ResourcesByType) { + if task.dumpResources { + do { + try createOutputDirectory(task: task) + } catch let error { + print(error.shortDescription(withUnderlyingError: true)) + exit(EXIT_FAILURE) + } + } + + let filteredResourcesByType = filter(resources: resourcesByType, task: task) + for (_, resources) in filteredResourcesByType { + for resource in resources { + print(format(resource)) + + if task.printResources { + print(format(resource.data)) + } + + if task.dumpResources { + dump(task: task, resource: resource) + } + } + } +} + +enum OutputDirectoryError: NestingSwresError { + case directoryIsNotAFolder + case directoryExists + case couldntCreateDirectory(underlyingError: Error) + + var underlyingError: Error? { + switch self { + case .couldntCreateDirectory(let underlyingError): + return underlyingError + default: + return nil + } + } + + var description: String { + switch self { + case .directoryIsNotAFolder: + return "Output directory is not a folder." + case .directoryExists: + return "Output directory already exists. Use -f to overwrite existing files." + case .couldntCreateDirectory: + return "Couldn't create output directory." + } + } +} + +func createOutputDirectory(task: ExplodeTask) throws { + let fileManager = FileManager.default + let outputDirectoryPath = task.outputFolder.path + + var isDirectory: ObjCBool = false + if fileManager.fileExists(atPath: outputDirectoryPath, isDirectory: &isDirectory) { + guard isDirectory.boolValue else { + throw OutputDirectoryError.directoryIsNotAFolder + } + + guard task.overwriteExistingFiles else { + throw OutputDirectoryError.directoryExists + } + } + + do { + try fileManager.createDirectory(at: task.outputFolder, withIntermediateDirectories: true, attributes: nil) + } catch let error { + throw OutputDirectoryError.couldntCreateDirectory(underlyingError: error) + } +} + +func dump(task: ExplodeTask, resource: Resource) { + let (folderURL, fileURL) = explodedLocation(task: task, resource: resource) + + let fileManager = FileManager.default + do { + try fileManager.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) + try resource.data.write(to: fileURL) + } catch let error { + print("Couldn't dump resource \(resource.type) \(resource.identifier).") + print(error.shortDescription) + } + + if task.translatorFilter >= TranslatorFilter.onlyLikelyTranslators { + let translatorManager = TranslatorManager.sharedInstance + let translationResults = translatorManager.translate(resource, includeTranslators: task.translatorFilter) + + for translationResult in translationResults { + switch translationResult { + case .translated(let translation): + let outputURL = fileURL.appendingPathExtension(translation.suggestedFileExtension) + let data = translation.data + do { + try data.write(to: outputURL) + } catch let error { + let resourceDescription = format(resource, short: true) + print("Couldn't write translation for resource \(resourceDescription).") + print(error.shortDescription(withUnderlyingError: true)) + } + case .error(let error): + let resourceDescription = format(resource, short: true) + print("Failed to translate resource \(resourceDescription).") + print(error.shortDescription(withUnderlyingError: true)) + } + } + } +} + +func filter(resources: ResourcesByType, task: ExplodeTask) -> ResourcesByType { + return resources.flatMap { (type: FourCharCode, resources: Array) -> (FourCharCode, Array)? in + if let typeFilter = task.typeFilter, type != typeFilter { + return nil + } + + let filteredResources = resources.flatMap { (resource: Resource) -> Resource? in + if let identifierFilter = task.identifierFilter, resource.identifier != identifierFilter { + return nil + } + return resource + } + + return (type, filteredResources) + } +} + +func format(_ resource: Resource, short: Bool = false) -> String { + if short { + return "'\(resource.type.description)' \(resource.identifier)" + } + + var string = String(format: "'%@' %7d %8d bytes", resource.type.description, resource.identifier, resource.data.count) + if let name = resource.stringName { + string += " \"\(name)\"" + } + return string +} + +func format(_ data: Data) -> String { + let options = MacOSRomanConversionOptions(filterControlCharacters: true, filterFilesystemUnsafeCharacters: false, filterNonASCIICharacters: true, replacementMacOSRomanByte: MacOSRomanByteFullStop) + + var lines = Array() + data.withUnsafeBytes { (unsafeBytes: UnsafePointer) in + offsetAndLengthStride(from: 0, to: data.count, by: 16, { (offset: Int, length: Int) in + let formattedOffset = format(asHex: offset, length: 8) + + let lineBuffer = Buffer(pointer: unsafeBytes + offset, count: length) + let formattedLine = format(line: lineBuffer, lineLength: 16) + let asciiFormattedBytes = stringFromMacOSRomanBytes(lineBuffer, options: options) + + let rowString = "\(formattedOffset): \(formattedLine) \(asciiFormattedBytes)" + lines.append(rowString) + }) + } + + return lines.joined(separator: "\n") +} + +// 0-9, A-F +let asciiHexCharacters: Array = [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66] +let asciiSpace: CChar = 0x20 + +func format(asHex number: Int, length: Int) -> String { + assert(length >= 0) + + var number = number + let stringBufferCapacity = length + 1 + let stringBuffer = UnsafeMutablePointer.allocate(capacity: stringBufferCapacity) + defer { + stringBuffer.deallocate(capacity: stringBufferCapacity) + } + + var stringBufferIterator = stringBuffer + length + stringBufferIterator.pointee = 0 + stringBufferIterator -= 1 + + while stringBufferIterator >= stringBuffer { + stringBufferIterator.pointee = asciiHexCharacters[number % 16] + number = number / 16 + stringBufferIterator -= 1 + } + + return String(cString: stringBuffer, encoding: String.Encoding.ascii)! +} + +func format(line lineBuffer: Buffer, lineLength: Int) -> String { + assert(lineBuffer.count <= lineLength) + + let lineBytes = lineBuffer.pointer + let lineBytesCount = lineBuffer.count + + let spacerCount = max(0, (lineLength - 1) / 2) + let stringBufferSize = lineLength * 2 + spacerCount + 1 + let stringBuffer = UnsafeMutablePointer.allocate(capacity: stringBufferSize) + defer { + stringBuffer.deallocate(capacity: stringBufferSize) + } + + var stringBufferIterator = stringBuffer + + @inline(__always) func writeCChar(_ char: CChar) { + stringBufferIterator.pointee = char + stringBufferIterator += 1 + } + + for byteIndex in 0.. 0 { + writeCChar(asciiSpace) + } + + if byteIndex < lineBytesCount { + let byte = lineBytes[byteIndex] + writeCChar(asciiHexCharacters[Int(byte / 16)]) + writeCChar(asciiHexCharacters[Int(byte % 16)]) + } else { + writeCChar(asciiSpace) + writeCChar(asciiSpace) + } + } + + writeCChar(0) + + return String(cString: stringBuffer, encoding: String.Encoding.ascii)! +} + +func explodedLocation(task: ExplodeTask, resource: Resource) -> (URL, URL) { + let outputFolder = task.outputFolder + let typeFolder = outputFolder.appendingPathComponent(filesystemSafeString(resource.type.bytes)) + var filename = "\(resource.identifier)" + if let name = resource.name { + let sanitizedName = filesystemSafeString(name) + filename += " \(sanitizedName)" + } + let url = typeFolder.appendingPathComponent(filename) + return (typeFolder, url) +} + +func swresExplodeMain() -> Int32 { + let task = taskForArguments() + return run(task) +} diff --git a/SwresExplode/SwresExplode.xcconfig b/SwresExplode/SwresExplode.xcconfig new file mode 100644 index 0000000..0e0ad21 --- /dev/null +++ b/SwresExplode/SwresExplode.xcconfig @@ -0,0 +1 @@ +PRODUCT_NAME = SwresExplode diff --git a/SwresExplode/main.swift b/SwresExplode/main.swift new file mode 100644 index 0000000..aca6c75 --- /dev/null +++ b/SwresExplode/main.swift @@ -0,0 +1,8 @@ +// +// main.swift +// SwresTools +// + +import Darwin + +exit(swresExplodeMain()) diff --git a/SwresFUSE.png b/SwresFUSE.png new file mode 100644 index 0000000..c0606cf Binary files /dev/null and b/SwresFUSE.png differ diff --git a/SwresFUSE/FilesystemNode.swift b/SwresFUSE/FilesystemNode.swift new file mode 100644 index 0000000..325de47 --- /dev/null +++ b/SwresFUSE/FilesystemNode.swift @@ -0,0 +1,113 @@ +// +// FilesystemNode.swift +// SwresTools +// + +import Foundation + +enum FilesystemNode { + case folder(name: StringAndCString, children: Dictionary) + case file(name: StringAndCString, data: Data) + + var cStringName: ManagedUnsafeMutablePointer { + switch self { + case .folder(let name, _): + return name.cString + case .file(let name, _): + return name.cString + } + } + + var name: String { + switch self { + case .folder(let name, _): + return name.string + case .file(let name, _): + return name.string + } + } + + init(name: String, data: Data) { + self = .file(name: StringAndCString(name), data: data) + } + + init(name: String, children: Array) { + let nameAndChildTuples = children.map { (child: FilesystemNode) -> (String, FilesystemNode) in + return (child.name, child) + } + self = .folder(name: StringAndCString(name), children: Dictionary(nameAndChildTuples)) + } + + func nodeAtPath(_ path: UnsafePointer) -> FilesystemNode? { + guard let pathString = String(cString: path, encoding: String.Encoding.utf8) else { + return nil + } + return nodeAtPath(pathString) + } + + func nodeAtPath(_ path: String) -> FilesystemNode? { + let pathComponents = path.components(separatedBy: "/").filter { (pathComponent: String) -> Bool in + pathComponent.characters.count > 0 + } + + return _nodeAtPath(pathComponents[0 ..< pathComponents.endIndex]) + } + + private func _nodeAtPath(_ pathComponents: ArraySlice) -> FilesystemNode? { + guard let nextComponent = pathComponents.first else { + return self + } + + switch self { + case .file: + guard pathComponents.count == 1 && nextComponent == self.name else { + return nil + } + return self + case .folder(_, let children): + guard let child = children[nextComponent] else { + return nil + } + return child._nodeAtPath(pathComponents[1 ..< pathComponents.endIndex]) + } + } + + func isFolder() -> Bool { + switch self { + case .folder: + return true + default: + return false + } + } + + func stLinkCount() -> nlink_t { + switch self { + case .file: + return 1 + case .folder(_, let children): + let childFolders = children.filter { (_, child: FilesystemNode) in + return child.isFolder() + } + return nlink_t(childFolders.count + 2) + } + } + + func stMode() -> mode_t { + switch self { + case .file: + return S_IFREG | 0o0444 + case .folder: + return S_IFDIR | 0o0555 + } + } + + func stSize() -> off_t { + switch self { + case .file(_, let data): + return off_t(data.count) + case .folder: + return 0 + } + } +} diff --git a/SwresFUSE/SwresFUSE.xcconfig b/SwresFUSE/SwresFUSE.xcconfig new file mode 100644 index 0000000..3e87d71 --- /dev/null +++ b/SwresFUSE/SwresFUSE.xcconfig @@ -0,0 +1,6 @@ +PRODUCT_NAME = SwresFUSE + +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) FUSE_USE_VERSION=26 _FILE_OFFSET_BITS=64 + +HEADER_SEARCH_PATHS = $(inherited) /usr/local/include/osxfuse +LIBRARY_SEARCH_PATHS = $(inherited) /usr/local/lib diff --git a/SwresFUSE/SwresFuse.swift b/SwresFUSE/SwresFuse.swift new file mode 100644 index 0000000..7a1667a --- /dev/null +++ b/SwresFUSE/SwresFuse.swift @@ -0,0 +1,324 @@ +// +// SwresFuse.swift +// SwresTools +// + +import Foundation +import Fuse + +struct FuseTask { + var inputURL: URL? + var includeTranslations: Bool = false + var allowPossibleTranslations: Bool = false + var fuseArgs: ManagedUnsafeMutablePointer? +} + +struct CommandLineOption { + let template: String + let key: CommandLineKey +} + +enum CommandLineKey: Int32 { + case fuse_opt_key_keep = -3 + case fuse_opt_key_nonopt = -2 + case fuse_opt_key_opt = -1 + case help = 1 + case includeTranslations + case allowPossibleTranslations +} + +var rootNode: FilesystemNode? +let currentDirectoryCString = ".".copyCString() +let parentDirectoryCString = "..".copyCString() + +func printUsageAndExit(status: Int32 = EXIT_SUCCESS) -> Never { + let processName = ProcessInfo.processInfo.processName + + print("Mount a resource fork with FUSE.") + print("Usage: \(processName) [options] resourcefile mountpoint") + print("Options:") + print(" -h Show this help message") + print(" -c Attempt to convert resources into more modern or portable formats.") + print(" -C Also use best guess conversions.") + + exit(status) +} + +func dieWithMessage(_ message: String) -> Never { + print(message) + exit(EXIT_FAILURE) +} + +func withFuseOptions(_ options: Array, _ block: (UnsafePointer) -> Void) { + var cStringPool = Array>() + + var fuseOptions = ContiguousArray() + fuseOptions.reserveCapacity(options.count) + + for option in options { + let cString = option.template.copyCString() + cStringPool.append(cString) + + let fuseOption = fuse_opt(templ: UnsafePointer(cString.pointer), offset: UInt(UInt32(bitPattern: -1)), value: option.key.rawValue) + fuseOptions.append(fuseOption) + } + + let nullOption = fuse_opt(templ: nil, offset: 0, value: 0) + fuseOptions.append(nullOption) + + fuseOptions.withUnsafeBufferPointer { (fuseOptionsBufferPointer: UnsafeBufferPointer) in + guard let fuseOptionsPointer = fuseOptionsBufferPointer.baseAddress else { + dieWithMessage("Error setting up option parsing.") + } + block(fuseOptionsPointer) + } +} + +func taskForArguments() -> FuseTask { + var task = FuseTask() + + let options = [ + CommandLineOption(template: "-h", key: CommandLineKey.help), + CommandLineOption(template: "-c", key: CommandLineKey.includeTranslations), + CommandLineOption(template: "-C", key: CommandLineKey.allowPossibleTranslations), + CommandLineOption(template: "-d", key: CommandLineKey.fuse_opt_key_keep), + ] + + let args = malloc(MemoryLayout.size).assumingMemoryBound(to: fuse_args.self) + args.pointee.argc = CommandLine.argc + args.pointee.argv = CommandLine.unsafeArgv + args.pointee.allocated = 0 + + withFuseOptions(options, { (fuseOptions: UnsafePointer) in + let parseResult = fuse_opt_parse(args, &task, fuseOptions, { (context: UnsafeMutableRawPointer?, arg: UnsafePointer?, key: Int32, args: UnsafeMutablePointer?) -> Int32 in + guard let taskRawPointer = UnsafeMutableRawPointer(context) else { + dieWithMessage("Error parsing arguments. Received a NULL context pointer from FUSE.") + } + let taskPointer = taskRawPointer.bindMemory(to: FuseTask.self, capacity: 1) + + guard let arg = arg else { + dieWithMessage("Error parsing arguments. Received a NULL argument from FUSE.") + } + guard let option = String(cString: arg, encoding: String.Encoding.ascii) else { + dieWithMessage("Error parsing arguments. Argument encoding unrecognized.") + } + + guard let key = CommandLineKey(rawValue: key) else { + dieWithMessage("Unexpected key from FUSE.") + } + + switch key { + case .fuse_opt_key_nonopt: + if taskPointer.pointee.inputURL == nil { + taskPointer.pointee.inputURL = URL(fileURLWithPath: option) + return 0 + } + return 1 + case .fuse_opt_key_opt: + print("Unrecognized option \(option).") + printUsageAndExit(status: EXIT_FAILURE) + case .help: + printUsageAndExit() + case .includeTranslations: + taskPointer.pointee.includeTranslations = true + return 0 + case .allowPossibleTranslations: + taskPointer.pointee.includeTranslations = true + taskPointer.pointee.allowPossibleTranslations = true + return 0 + default: + dieWithMessage("Unexpected key from FUSE.") + } + return 1 + }) + + if parseResult != 0 { + print("Failed to parse optinos.") + exit(EXIT_FAILURE) + } + }) + + fuse_opt_add_arg(args, "-s") + fuse_opt_add_arg(args, "-f") + + task.fuseArgs = ManagedUnsafeMutablePointer(adoptPointer: args) + return task +} + +func getAttr(path: UnsafePointer?, stbuf: UnsafeMutablePointer?) -> Int32 { + guard let rootNode = rootNode else { + dieWithMessage("No filesystem root note was created.") + } + guard let path = path, let stbuf = stbuf else { + dieWithMessage("Received null parameter from FUSE.") + } + + guard let node = rootNode.nodeAtPath(path) else { + return -ENOENT + } + + stbuf.pointee.st_mode = node.stMode() + stbuf.pointee.st_nlink = node.stLinkCount() + stbuf.pointee.st_size = node.stSize() + stbuf.pointee.st_uid = getuid() + stbuf.pointee.st_gid = getgid() + + return 0 +} + +func readDir(path: UnsafePointer?, buf: UnsafeMutableRawPointer?, filler: fuse_fill_dir_t?, offset: off_t, fi: UnsafeMutablePointer?) -> Int32 { + guard let rootNode = rootNode else { + dieWithMessage("No filesystem root note was created.") + } + guard let path = path, let buf = buf, let filler = filler else { + dieWithMessage("Received null parameter from FUSE.") + } + + guard let node = rootNode.nodeAtPath(path) else { + return -ENOENT + } + + @inline(__always) func appendEntry(filename: ManagedUnsafeMutablePointer) { + guard filler(buf, filename.pointer, nil, 0) == 0 else { + // TODO: Figure out how to correctly support large directories. + dieWithMessage("readDir buffer is full.") + } + } + + switch node { + case .file: + return -ENOENT + case .folder(_, let children): + appendEntry(filename: parentDirectoryCString) + appendEntry(filename: currentDirectoryCString) + for (_, child) in children { + appendEntry(filename: child.cStringName) + } + } + + return 0 +} + +func openFile(path: UnsafePointer?, fi: UnsafeMutablePointer?) -> Int32 { + guard let rootNode = rootNode else { + dieWithMessage("No filesystem root note was created.") + } + guard let path = path, let fi = fi else { + dieWithMessage("Received null parameter from FUSE.") + } + + guard let _ = rootNode.nodeAtPath(path) else { + return -ENOENT + } + + guard fi.pointee.flags & 3 == O_RDONLY else { + return -EACCES + } + + return 0 +} + +func readFile(path: UnsafePointer?, buf: UnsafeMutablePointer?, size: size_t, offset: off_t, fi: UnsafeMutablePointer?) -> Int32 { + guard let rootNode = rootNode else { + dieWithMessage("No filesystem root note was created.") + } + guard let path = path else { + dieWithMessage("Received null parameter from FUSE.") + } + + guard let node = rootNode.nodeAtPath(path) else { + return -ENOENT + } + + switch node { + case .folder: + return -ENOENT + case .file(_, let data): + let length = data.count + let offset = Int(offset) + guard length > offset else { + return 0 + } + let bytesCopied = min(length - offset, size) + data.withUnsafeBytes { (dataBytes: UnsafePointer) -> Void in + memcpy(buf, dataBytes + offset, bytesCopied) + } + return Int32(bytesCopied) + } +} + +func run(_ task: FuseTask) -> Int32 { + guard let inputURL = task.inputURL else { + dieWithMessage("Missing inputURL in task.") + } + + do { + let resourcesByType = try readResourceFork(inputURL) + rootNode = filesystemNode(resourcesByType, includeTranslations: task.includeTranslations) + + var operations = fuse_operations() + operations.getattr = getAttr + operations.readdir = readDir + operations.open = openFile + operations.read = readFile + + guard let args = task.fuseArgs?.pointer else { + dieWithMessage("Failed to construct arguments to pass to FUSE.") + } + + let result = fuse_main_real(args.pointee.argc, args.pointee.argv, &operations, MemoryLayout.size(ofValue: operations), nil) + print("hi") + return result + } catch { + dieWithMessage(error.shortDescription(withUnderlyingError: true)) + } +} + +func filesystemNode(_ resourcesByType: ResourcesByType, includeTranslations: Bool) -> FilesystemNode { + let folders = resourcesByType.map { (type: FourCharCode, resources: Array) -> FilesystemNode in + let folderName = filesystemSafeString(type.bytes) + let children = resources.flatMap { (resource: Resource) -> Array in + return filesystemNodes(resource, includeTranslations: includeTranslations) + } + + return FilesystemNode(name: folderName, children: children) + } + + return FilesystemNode(name: "ROOT", children: folders) +} + +func filesystemNodes(_ resource: Resource, includeTranslations: Bool) -> Array { + var nodes = Array() + + var filename = "\(resource.identifier)" + if let name = resource.name { + let sanitizedName = filesystemSafeString(name) + filename += " \(sanitizedName)" + } + nodes.append(FilesystemNode(name: filename, data: resource.data)) + + if (includeTranslations) { + let translatorManager = TranslatorManager.sharedInstance + let translationResults = translatorManager.translate(resource, includeTranslators: TranslatorFilter.likelyAndPossibleTranslators) + + let translationNodes = translationResults.flatMap { (translationResult: TranslationResult) -> FilesystemNode? in + switch translationResult { + case .translated(let translation): + let translatedFilename = filename + ".\(translation.suggestedFileExtension)" + return FilesystemNode(name: translatedFilename, data: translation.data) + case .error(let error): + print(error.shortDescription(withUnderlyingError: true)) + return nil + } + } + + nodes.append(contentsOf: translationNodes) + } + + return nodes +} + +func swresFuseMain() -> Int32 { + let task = taskForArguments() + return run(task) +} diff --git a/SwresFUSE/main.swift b/SwresFUSE/main.swift new file mode 100644 index 0000000..2272b4b --- /dev/null +++ b/SwresFUSE/main.swift @@ -0,0 +1,8 @@ +// +// main.swift +// SwresFUSE +// + +import Darwin + +exit(swresFuseMain()) diff --git a/SwresTools.xcodeproj/project.pbxproj b/SwresTools.xcodeproj/project.pbxproj new file mode 100644 index 0000000..74d9215 --- /dev/null +++ b/SwresTools.xcodeproj/project.pbxproj @@ -0,0 +1,535 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + CA9567C41DC132620031E5F5 /* Everything */ = { + isa = PBXAggregateTarget; + buildConfigurationList = CA9567C51DC132620031E5F5 /* Build configuration list for PBXAggregateTarget "Everything" */; + buildPhases = ( + ); + dependencies = ( + CA9567C91DC1326A0031E5F5 /* PBXTargetDependency */, + CA9567CB1DC1326C0031E5F5 /* PBXTargetDependency */, + ); + name = Everything; + productName = Everything; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + CA2B20FF1DA57DAB00A14B92 /* TranslatorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2B20FE1DA57DAB00A14B92 /* TranslatorManager.swift */; }; + CA4150EA1DAF4401005F689D /* SequenceExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E41DAF4401005F689D /* SequenceExtras.swift */; }; + CA4150EB1DAF4401005F689D /* DictionaryExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E51DAF4401005F689D /* DictionaryExtras.swift */; }; + CA4150EC1DAF4401005F689D /* ErrorExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E61DAF4401005F689D /* ErrorExtras.swift */; }; + CA4150ED1DAF4401005F689D /* PointerExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E71DAF4401005F689D /* PointerExtras.swift */; }; + CA4150EE1DAF4401005F689D /* StrideExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E81DAF4401005F689D /* StrideExtras.swift */; }; + CA4150EF1DAF4401005F689D /* StringExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E91DAF4401005F689D /* StringExtras.swift */; }; + CA4150F21DAF4407005F689D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150F01DAF4407005F689D /* main.swift */; }; + CA4150F31DAF4407005F689D /* SwresExplode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150F11DAF4407005F689D /* SwresExplode.swift */; }; + CA4150F61DAF440F005F689D /* PascalStringTranslator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150F41DAF440F005F689D /* PascalStringTranslator.swift */; }; + CA4150F71DAF440F005F689D /* SndTranslator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150F51DAF440F005F689D /* SndTranslator.swift */; }; + CA4EB2CE1DB8057100A775DF /* SequenceExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E41DAF4401005F689D /* SequenceExtras.swift */; }; + CA4EB2CF1DB8057100A775DF /* DictionaryExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E51DAF4401005F689D /* DictionaryExtras.swift */; }; + CA4EB2D01DB8057100A775DF /* ErrorExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E61DAF4401005F689D /* ErrorExtras.swift */; }; + CA4EB2D11DB8057100A775DF /* PointerExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E71DAF4401005F689D /* PointerExtras.swift */; }; + CA4EB2D21DB8057100A775DF /* StrideExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E81DAF4401005F689D /* StrideExtras.swift */; }; + CA4EB2D31DB8057100A775DF /* StringExtras.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150E91DAF4401005F689D /* StringExtras.swift */; }; + CA4EB2D41DB8057700A775DF /* FourCharCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9007D01D8DBD1900B7D2BD /* FourCharCode.swift */; }; + CA4EB2D51DB8057700A775DF /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9007D41D8DF9C400B7D2BD /* Resource.swift */; }; + CA4EB2D61DB8057700A775DF /* ResourceForkReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9007CE1D8DBCAF00B7D2BD /* ResourceForkReader.swift */; }; + CA4EB2D71DB8057700A775DF /* SeekableReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9007CC1D8DB31400B7D2BD /* SeekableReader.swift */; }; + CA4EB2D81DB8057700A775DF /* Writer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA572C501DB2058E00A2AF8F /* Writer.swift */; }; + CA4EB2D91DB8057700A775DF /* TranslatorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2B20FE1DA57DAB00A14B92 /* TranslatorManager.swift */; }; + CA4EB2DA1DB8057700A775DF /* PascalStringTranslator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150F41DAF440F005F689D /* PascalStringTranslator.swift */; }; + CA4EB2DB1DB8057700A775DF /* SndTranslator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4150F51DAF440F005F689D /* SndTranslator.swift */; }; + CA4EB2DC1DB8057700A775DF /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6136641DA4ADD0001F81DA /* Translator.swift */; }; + CA4EB2E11DB9ECCB00A775DF /* FilesystemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4EB2DF1DB9E44300A775DF /* FilesystemNode.swift */; }; + CA572C511DB2058E00A2AF8F /* Writer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA572C501DB2058E00A2AF8F /* Writer.swift */; }; + CA584C971DB491DF00E36C26 /* SwresFuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA584C961DB491DF00E36C26 /* SwresFuse.swift */; }; + CA5AA8EC1DB44814001866EB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5AA8EB1DB44814001866EB /* main.swift */; }; + CA6136651DA4ADD0001F81DA /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6136641DA4ADD0001F81DA /* Translator.swift */; }; + CA9007CD1D8DB31400B7D2BD /* SeekableReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9007CC1D8DB31400B7D2BD /* SeekableReader.swift */; }; + CA9007CF1D8DBCAF00B7D2BD /* ResourceForkReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9007CE1D8DBCAF00B7D2BD /* ResourceForkReader.swift */; }; + CA9007D11D8DBD1900B7D2BD /* FourCharCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9007D01D8DBD1900B7D2BD /* FourCharCode.swift */; }; + CA9007D51D8DF9C400B7D2BD /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9007D41D8DF9C400B7D2BD /* Resource.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + CA9567C81DC1326A0031E5F5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CA9007BA1D8DAFB500B7D2BD /* Project object */; + proxyType = 1; + remoteGlobalIDString = CA9007C11D8DAFB500B7D2BD; + remoteInfo = SwresExplode; + }; + CA9567CA1DC1326C0031E5F5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CA9007BA1D8DAFB500B7D2BD /* Project object */; + proxyType = 1; + remoteGlobalIDString = CA5AA8E81DB44814001866EB; + remoteInfo = SwresFUSE; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + CA5AA8E71DB44814001866EB /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; + CA9007C01D8DAFB500B7D2BD /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + CA2B20FE1DA57DAB00A14B92 /* TranslatorManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TranslatorManager.swift; path = Translators/TranslatorManager.swift; sourceTree = ""; }; + CA4150E41DAF4401005F689D /* SequenceExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SequenceExtras.swift; path = Extras/SequenceExtras.swift; sourceTree = ""; }; + CA4150E51DAF4401005F689D /* DictionaryExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DictionaryExtras.swift; path = Extras/DictionaryExtras.swift; sourceTree = ""; }; + CA4150E61DAF4401005F689D /* ErrorExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ErrorExtras.swift; path = Extras/ErrorExtras.swift; sourceTree = ""; }; + CA4150E71DAF4401005F689D /* PointerExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PointerExtras.swift; path = Extras/PointerExtras.swift; sourceTree = ""; }; + CA4150E81DAF4401005F689D /* StrideExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StrideExtras.swift; path = Extras/StrideExtras.swift; sourceTree = ""; }; + CA4150E91DAF4401005F689D /* StringExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StringExtras.swift; path = Extras/StringExtras.swift; sourceTree = ""; }; + CA4150F01DAF4407005F689D /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = SwresExplode/main.swift; sourceTree = ""; }; + CA4150F11DAF4407005F689D /* SwresExplode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwresExplode.swift; path = SwresExplode/SwresExplode.swift; sourceTree = ""; }; + CA4150F41DAF440F005F689D /* PascalStringTranslator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PascalStringTranslator.swift; path = Translators/PascalStringTranslator.swift; sourceTree = ""; }; + CA4150F51DAF440F005F689D /* SndTranslator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SndTranslator.swift; path = Translators/SndTranslator.swift; sourceTree = ""; }; + CA4EB2DF1DB9E44300A775DF /* FilesystemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesystemNode.swift; sourceTree = ""; }; + CA572C501DB2058E00A2AF8F /* Writer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Writer.swift; sourceTree = ""; }; + CA584C8C1DB47C9300E36C26 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; name = module.modulemap; path = Modules/FUSE/module.modulemap; sourceTree = ""; }; + CA584C8F1DB4894500E36C26 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Configurations/Debug.xcconfig; sourceTree = ""; }; + CA584C901DB4894500E36C26 /* Project.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Project.xcconfig; path = Configurations/Project.xcconfig; sourceTree = ""; }; + CA584C911DB4894500E36C26 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Configurations/Release.xcconfig; sourceTree = ""; }; + CA584C941DB4897800E36C26 /* SwresExplode.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = SwresExplode.xcconfig; path = SwresExplode/SwresExplode.xcconfig; sourceTree = ""; }; + CA584C951DB4897F00E36C26 /* SwresFUSE.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SwresFUSE.xcconfig; sourceTree = ""; }; + CA584C961DB491DF00E36C26 /* SwresFuse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwresFuse.swift; sourceTree = ""; }; + CA5AA8E91DB44814001866EB /* SwresFUSE */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = SwresFUSE; sourceTree = BUILT_PRODUCTS_DIR; }; + CA5AA8EB1DB44814001866EB /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + CA6136641DA4ADD0001F81DA /* Translator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Translator.swift; path = Translators/Translator.swift; sourceTree = ""; }; + CA9007C21D8DAFB500B7D2BD /* SwresExplode */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = SwresExplode; sourceTree = BUILT_PRODUCTS_DIR; }; + CA9007CC1D8DB31400B7D2BD /* SeekableReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeekableReader.swift; sourceTree = ""; }; + CA9007CE1D8DBCAF00B7D2BD /* ResourceForkReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceForkReader.swift; sourceTree = ""; }; + CA9007D01D8DBD1900B7D2BD /* FourCharCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FourCharCode.swift; sourceTree = ""; }; + CA9007D41D8DF9C400B7D2BD /* Resource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CA5AA8E61DB44814001866EB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA9007BF1D8DAFB500B7D2BD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + CA4150E31DAF439C005F689D /* SwresExpolode */ = { + isa = PBXGroup; + children = ( + CA4150F01DAF4407005F689D /* main.swift */, + CA4150F11DAF4407005F689D /* SwresExplode.swift */, + CA584C941DB4897800E36C26 /* SwresExplode.xcconfig */, + ); + name = SwresExpolode; + sourceTree = ""; + }; + CA584C8D1DB47C9F00E36C26 /* Modules */ = { + isa = PBXGroup; + children = ( + CA5AA8F01DB44E40001866EB /* FUSE */, + ); + name = Modules; + sourceTree = ""; + }; + CA584C8E1DB4893300E36C26 /* Configurations */ = { + isa = PBXGroup; + children = ( + CA584C8F1DB4894500E36C26 /* Debug.xcconfig */, + CA584C901DB4894500E36C26 /* Project.xcconfig */, + CA584C911DB4894500E36C26 /* Release.xcconfig */, + ); + name = Configurations; + sourceTree = ""; + }; + CA5AA8EA1DB44814001866EB /* SwresFUSE */ = { + isa = PBXGroup; + children = ( + CA5AA8EB1DB44814001866EB /* main.swift */, + CA4EB2DF1DB9E44300A775DF /* FilesystemNode.swift */, + CA584C961DB491DF00E36C26 /* SwresFuse.swift */, + CA584C951DB4897F00E36C26 /* SwresFUSE.xcconfig */, + ); + path = SwresFUSE; + sourceTree = ""; + }; + CA5AA8F01DB44E40001866EB /* FUSE */ = { + isa = PBXGroup; + children = ( + CA584C8C1DB47C9300E36C26 /* module.modulemap */, + ); + name = FUSE; + sourceTree = ""; + }; + CA6136601DA49D48001F81DA /* Extras */ = { + isa = PBXGroup; + children = ( + CA4150E51DAF4401005F689D /* DictionaryExtras.swift */, + CA4150E61DAF4401005F689D /* ErrorExtras.swift */, + CA4150E71DAF4401005F689D /* PointerExtras.swift */, + CA4150E41DAF4401005F689D /* SequenceExtras.swift */, + CA4150E81DAF4401005F689D /* StrideExtras.swift */, + CA4150E91DAF4401005F689D /* StringExtras.swift */, + ); + name = Extras; + sourceTree = ""; + }; + CA6136611DA4ADA2001F81DA /* Translators */ = { + isa = PBXGroup; + children = ( + CA4150F41DAF440F005F689D /* PascalStringTranslator.swift */, + CA4150F51DAF440F005F689D /* SndTranslator.swift */, + CA6136641DA4ADD0001F81DA /* Translator.swift */, + CA2B20FE1DA57DAB00A14B92 /* TranslatorManager.swift */, + ); + name = Translators; + sourceTree = ""; + }; + CA9007B91D8DAFB500B7D2BD = { + isa = PBXGroup; + children = ( + CA6136601DA49D48001F81DA /* Extras */, + CA9007C41D8DAFB500B7D2BD /* SwresTools */, + CA6136611DA4ADA2001F81DA /* Translators */, + CA4150E31DAF439C005F689D /* SwresExpolode */, + CA5AA8EA1DB44814001866EB /* SwresFUSE */, + CA584C8D1DB47C9F00E36C26 /* Modules */, + CA584C8E1DB4893300E36C26 /* Configurations */, + CA9007C31D8DAFB500B7D2BD /* Products */, + ); + sourceTree = ""; + }; + CA9007C31D8DAFB500B7D2BD /* Products */ = { + isa = PBXGroup; + children = ( + CA9007C21D8DAFB500B7D2BD /* SwresExplode */, + CA5AA8E91DB44814001866EB /* SwresFUSE */, + ); + name = Products; + sourceTree = ""; + }; + CA9007C41D8DAFB500B7D2BD /* SwresTools */ = { + isa = PBXGroup; + children = ( + CA9007D01D8DBD1900B7D2BD /* FourCharCode.swift */, + CA9007D41D8DF9C400B7D2BD /* Resource.swift */, + CA9007CE1D8DBCAF00B7D2BD /* ResourceForkReader.swift */, + CA9007CC1D8DB31400B7D2BD /* SeekableReader.swift */, + CA572C501DB2058E00A2AF8F /* Writer.swift */, + ); + path = SwresTools; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CA5AA8E81DB44814001866EB /* SwresFUSE */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA5AA8EF1DB44814001866EB /* Build configuration list for PBXNativeTarget "SwresFUSE" */; + buildPhases = ( + CA5AA8E51DB44814001866EB /* Sources */, + CA5AA8E61DB44814001866EB /* Frameworks */, + CA5AA8E71DB44814001866EB /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwresFUSE; + productName = SwresFUSE; + productReference = CA5AA8E91DB44814001866EB /* SwresFUSE */; + productType = "com.apple.product-type.tool"; + }; + CA9007C11D8DAFB500B7D2BD /* SwresExplode */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA9007C91D8DAFB500B7D2BD /* Build configuration list for PBXNativeTarget "SwresExplode" */; + buildPhases = ( + CA9007BE1D8DAFB500B7D2BD /* Sources */, + CA9007BF1D8DAFB500B7D2BD /* Frameworks */, + CA9007C01D8DAFB500B7D2BD /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwresExplode; + productName = SwresTools; + productReference = CA9007C21D8DAFB500B7D2BD /* SwresExplode */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CA9007BA1D8DAFB500B7D2BD /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0810; + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = "Paul Knight"; + TargetAttributes = { + CA5AA8E81DB44814001866EB = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + CA9007C11D8DAFB500B7D2BD = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + CA9567C41DC132620031E5F5 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = CA9007BD1D8DAFB500B7D2BD /* Build configuration list for PBXProject "SwresTools" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = CA9007B91D8DAFB500B7D2BD; + productRefGroup = CA9007C31D8DAFB500B7D2BD /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CA9567C41DC132620031E5F5 /* Everything */, + CA9007C11D8DAFB500B7D2BD /* SwresExplode */, + CA5AA8E81DB44814001866EB /* SwresFUSE */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + CA5AA8E51DB44814001866EB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA4EB2D11DB8057100A775DF /* PointerExtras.swift in Sources */, + CA4EB2D21DB8057100A775DF /* StrideExtras.swift in Sources */, + CA4EB2D71DB8057700A775DF /* SeekableReader.swift in Sources */, + CA4EB2DC1DB8057700A775DF /* Translator.swift in Sources */, + CA4EB2D01DB8057100A775DF /* ErrorExtras.swift in Sources */, + CA4EB2D51DB8057700A775DF /* Resource.swift in Sources */, + CA4EB2D31DB8057100A775DF /* StringExtras.swift in Sources */, + CA5AA8EC1DB44814001866EB /* main.swift in Sources */, + CA4EB2E11DB9ECCB00A775DF /* FilesystemNode.swift in Sources */, + CA4EB2CE1DB8057100A775DF /* SequenceExtras.swift in Sources */, + CA4EB2CF1DB8057100A775DF /* DictionaryExtras.swift in Sources */, + CA584C971DB491DF00E36C26 /* SwresFuse.swift in Sources */, + CA4EB2D91DB8057700A775DF /* TranslatorManager.swift in Sources */, + CA4EB2D81DB8057700A775DF /* Writer.swift in Sources */, + CA4EB2D61DB8057700A775DF /* ResourceForkReader.swift in Sources */, + CA4EB2DB1DB8057700A775DF /* SndTranslator.swift in Sources */, + CA4EB2D41DB8057700A775DF /* FourCharCode.swift in Sources */, + CA4EB2DA1DB8057700A775DF /* PascalStringTranslator.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA9007BE1D8DAFB500B7D2BD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6136651DA4ADD0001F81DA /* Translator.swift in Sources */, + CA4150F21DAF4407005F689D /* main.swift in Sources */, + CA9007CD1D8DB31400B7D2BD /* SeekableReader.swift in Sources */, + CA4150EA1DAF4401005F689D /* SequenceExtras.swift in Sources */, + CA4150F31DAF4407005F689D /* SwresExplode.swift in Sources */, + CA9007CF1D8DBCAF00B7D2BD /* ResourceForkReader.swift in Sources */, + CA9007D11D8DBD1900B7D2BD /* FourCharCode.swift in Sources */, + CA2B20FF1DA57DAB00A14B92 /* TranslatorManager.swift in Sources */, + CA4150EB1DAF4401005F689D /* DictionaryExtras.swift in Sources */, + CA4150EF1DAF4401005F689D /* StringExtras.swift in Sources */, + CA4150EC1DAF4401005F689D /* ErrorExtras.swift in Sources */, + CA4150EE1DAF4401005F689D /* StrideExtras.swift in Sources */, + CA572C511DB2058E00A2AF8F /* Writer.swift in Sources */, + CA4150ED1DAF4401005F689D /* PointerExtras.swift in Sources */, + CA4150F71DAF440F005F689D /* SndTranslator.swift in Sources */, + CA9007D51D8DF9C400B7D2BD /* Resource.swift in Sources */, + CA4150F61DAF440F005F689D /* PascalStringTranslator.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + CA9567C91DC1326A0031E5F5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CA9007C11D8DAFB500B7D2BD /* SwresExplode */; + targetProxy = CA9567C81DC1326A0031E5F5 /* PBXContainerItemProxy */; + }; + CA9567CB1DC1326C0031E5F5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CA5AA8E81DB44814001866EB /* SwresFUSE */; + targetProxy = CA9567CA1DC1326C0031E5F5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + CA5AA8ED1DB44814001866EB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA584C951DB4897F00E36C26 /* SwresFUSE.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + CA5AA8EE1DB44814001866EB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA584C951DB4897F00E36C26 /* SwresFUSE.xcconfig */; + buildSettings = { + }; + name = Release; + }; + CA9007C71D8DAFB500B7D2BD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA584C8F1DB4894500E36C26 /* Debug.xcconfig */; + buildSettings = { + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + }; + name = Debug; + }; + CA9007C81D8DAFB500B7D2BD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA584C911DB4894500E36C26 /* Release.xcconfig */; + buildSettings = { + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + }; + name = Release; + }; + CA9007CA1D8DAFB500B7D2BD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA584C941DB4897800E36C26 /* SwresExplode.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + CA9007CB1D8DAFB500B7D2BD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA584C941DB4897800E36C26 /* SwresExplode.xcconfig */; + buildSettings = { + }; + name = Release; + }; + CA9567C61DC132620031E5F5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + CA9567C71DC132620031E5F5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CA5AA8EF1DB44814001866EB /* Build configuration list for PBXNativeTarget "SwresFUSE" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA5AA8ED1DB44814001866EB /* Debug */, + CA5AA8EE1DB44814001866EB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA9007BD1D8DAFB500B7D2BD /* Build configuration list for PBXProject "SwresTools" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA9007C71D8DAFB500B7D2BD /* Debug */, + CA9007C81D8DAFB500B7D2BD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA9007C91D8DAFB500B7D2BD /* Build configuration list for PBXNativeTarget "SwresExplode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA9007CA1D8DAFB500B7D2BD /* Debug */, + CA9007CB1D8DAFB500B7D2BD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA9567C51DC132620031E5F5 /* Build configuration list for PBXAggregateTarget "Everything" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA9567C61DC132620031E5F5 /* Debug */, + CA9567C71DC132620031E5F5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = CA9007BA1D8DAFB500B7D2BD /* Project object */; +} diff --git a/SwresTools.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SwresTools.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..7c55926 --- /dev/null +++ b/SwresTools.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SwresTools/FourCharCode.swift b/SwresTools/FourCharCode.swift new file mode 100644 index 0000000..f27790b --- /dev/null +++ b/SwresTools/FourCharCode.swift @@ -0,0 +1,50 @@ +// +// FourCharCode.swift +// SwresTools +// + +import Foundation + +enum FourCharCodeError: SwresError { + case invalidSequence + + var description: String { + return "Cannot construct FourCharCode from invalid byte sequence." + } +} + +struct FourCharCode: Equatable, Hashable, CustomStringConvertible { + let bytes: Data + + init(_ bytes: Data) throws { + guard bytes.count == 4 else { + throw FourCharCodeError.invalidSequence + } + self.bytes = bytes + } + + init(_ cString: UnsafeMutablePointer) throws { + let string = String(cString: cString) + try self.init(string) + } + + init(_ string: String) throws { + guard let data = string.data(using: String.Encoding.utf8) else { + throw FourCharCodeError.invalidSequence + } + try self.init(data) + } + + static func ==(lhs: FourCharCode, rhs: FourCharCode) -> Bool { + return lhs.bytes == rhs.bytes + } + + var hashValue: Int { + return (Int(bytes[0]) << 24) + (Int(bytes[1]) << 16) + (Int(bytes[2]) << 8) + (Int(bytes[3])) + } + + var description: String { + let options = MacOSRomanConversionOptions(filterControlCharacters: true, filterFilesystemUnsafeCharacters: false, filterNonASCIICharacters: false, replacementMacOSRomanByte: MacOSRomanByteQuestionMark) + return stringFromMacOSRomanBytes(bytes, options: options) + } +} diff --git a/SwresTools/Resource.swift b/SwresTools/Resource.swift new file mode 100644 index 0000000..66bf545 --- /dev/null +++ b/SwresTools/Resource.swift @@ -0,0 +1,27 @@ +// +// Resource.swift +// SwresTools +// + +import Foundation + +struct Resource: CustomStringConvertible { + let type: FourCharCode + let identifier: Int16 + let name: Data? + let data: Data + + var stringName: String? { + guard let name = name else { + return nil + } + + let options = MacOSRomanConversionOptions(filterControlCharacters: true, filterFilesystemUnsafeCharacters: false, filterNonASCIICharacters: false, replacementMacOSRomanByte: MacOSRomanByteQuestionMark) + return stringFromMacOSRomanBytes(name, options: options) + } + + var description: String { + let formattedName = stringName ?? "" + return "" + } +} diff --git a/SwresTools/ResourceForkReader.swift b/SwresTools/ResourceForkReader.swift new file mode 100644 index 0000000..b78e575 --- /dev/null +++ b/SwresTools/ResourceForkReader.swift @@ -0,0 +1,189 @@ +// +// ResourceManager.swift +// SwresTools +// + +// Resource Map documentation comes from Inside Macintosh: More Macintosh Toolbox (1993). + +import Foundation + +typealias ResourcesByType = Dictionary> + +enum ResourceForkReaderError: NestingSwresError { + case emptyResourceFork + case couldntReadResourceFork(underlyingError: Error?) + case invalidFormat(underlyingError: SeekableReaderError) + case other + + var description: String { + switch self { + case .emptyResourceFork: + return "The resource fork is empty." + case .couldntReadResourceFork(_): + return "Couldn't read resource fork." + case .invalidFormat(_): + return "Input file is corrupted or not a resource fork." + case .other : + return "An unexpected error happend while reading the resource fork." + } + } + + var underlyingError: Error? { + switch self { + case .couldntReadResourceFork(let subError): + return subError + case .invalidFormat(let subError): + return subError + default: + return nil + } + } +} + +// The resource header is: +// * Offset from beginning of resource fork to resource data (4) +// * Offset from beginning of resource fork to resource map (4) +// * Length of resource data (4) +// * Length of resource map (4) +func readResourceFork(_ path: URL) throws -> ResourcesByType { + let data = try _readResourceFork(path) + + do { + var reader = SeekableReader(data) + let dataOffset = try reader.readInt32() + let mapOffset = try reader.readInt32() + return try _parseResourceMap(reader: reader, dataOffset: dataOffset, mapOffset: mapOffset) + } catch let error as SeekableReaderError { + throw ResourceForkReaderError.invalidFormat(underlyingError: error) + } catch { + assertionFailure() + throw ResourceForkReaderError.other + } +} + +func _readResourceFork(_ path: URL) throws -> Data { + var error: Error? + + guard let data = ["..namedfork/rsrc", ""].firstSome({ (suffix: String) -> Data? in + let url = path.appendingPathComponent(suffix) + + var data: Data? + do { + data = try Data(contentsOf: url) + } catch let lastError { + error = lastError + } + + if data != nil && data!.count == 0 { + error = ResourceForkReaderError.emptyResourceFork + data = nil + } + + return data + }) else { + throw ResourceForkReaderError.couldntReadResourceFork(underlyingError: error) + } + + return data +} + +// The resource map is: +// * Reserved for copy of resource header (16) +// * Reserved for handle to next resource map (4) +// * Reserved for file reference number (2) +// * Resource fork attributes (2) +// * Offset from beginning of map to resource type list (2) [1] +// * Offset from beginning of map to resource name list (2) +// * Number of types in the map minus 1 (2) +// * Resource type list (Variable) +// * Reference lists (Variable) +// * Resource name list (Variable) +// +// Type list starts with: +// * Number of types in the map minus 1 +// Each type is: +// * Resource type (4) +// * Number of resources of this type in map minus 1 (2) +// * Offset from beginning of resource type list to reference list for this type (2) +// +// [1] Actually points to the type count, not the start of the variable length type list +func _parseResourceMap(reader: SeekableReader, dataOffset: Int32, mapOffset: Int32) throws -> ResourcesByType { + var reader = reader + var resourcesByType = ResourcesByType() + + try reader.seek(mapOffset) + try reader.skip(16 + 4 + 2 + 2) // Header copy, handle, file no., attributes + + let typeListOffset = mapOffset + Int32(try reader.readInt16()) + let nameListOffset = mapOffset + Int32(try reader.readInt16()) + + try reader.seek(typeListOffset) + let typeCount = try reader.readInt16() + 1 + + for _ in 1...typeCount { + let fourCharCodeBytes = try reader.readBytes(4) + let type = try FourCharCode(fourCharCodeBytes) + let resourceCount = try reader.readInt16() + 1 + let referenceListOffset = typeListOffset + Int32(try reader.readInt16()) + + resourcesByType[type] = try _parseReferenceList(reader: reader, dataOffset: dataOffset, referenceListOffset: referenceListOffset, nameListOffset: nameListOffset, type: type, resourceCount: resourceCount) + } + + return resourcesByType +} + +// A reference list entry is: +// * Resource ID (2) +// * Offset from beginning of resource name list to resource name (2) +// * Resource attributes (1) +// * Offset from beginning of resource data to data for this resource (3) +// * Reserved for handle to resource (4) +private func _parseReferenceList(reader: SeekableReader, dataOffset: Int32, referenceListOffset: Int32, nameListOffset: Int32, type: FourCharCode, resourceCount: Int16) throws -> Array { + var reader = reader + var resources = Array() + resources.reserveCapacity(Int(resourceCount)) + + try reader.seek(referenceListOffset) + for _ in 1...resourceCount { + let identifier = try reader.readInt16() + + var name: Data? + let nameOffset = try reader.readInt16() + if nameOffset != -1 { + let absoluteNameOffset = nameListOffset + Int32(nameOffset) + try name = _parseName(reader: reader, offset: absoluteNameOffset) + } + + try reader.skip(1) // Resource attributes + + let relativeDataOffset = try reader.readInt24() + let absoluteDataOffset = dataOffset + Int32(relativeDataOffset) + let data = try _parseResourceData(reader: reader, offset: absoluteDataOffset) + + let resource = Resource(type: type, identifier: identifier, name: name, data: data) + resources.append(resource) + + try reader.skip(4) // Handle to resource + } + + return resources +} + +// A name is: +// * Length of following resource name (1) +// * Characters of resource name (Variable) +private func _parseName(reader: SeekableReader, offset: Int32) throws -> Data { + var reader = reader + try reader.seek(offset) + let length = Int(try reader.readInt8()) + let bytes = try reader.readBytes(length) + return bytes +} + +private func _parseResourceData(reader: SeekableReader, offset: Int32) throws -> Data { + var reader = reader + try reader.seek(offset) + + let length = try reader.readInt32() + return try reader.readBytes(length) +} diff --git a/SwresTools/SeekableReader.swift b/SwresTools/SeekableReader.swift new file mode 100644 index 0000000..0db5c65 --- /dev/null +++ b/SwresTools/SeekableReader.swift @@ -0,0 +1,103 @@ +// +// SeekableReader.swift +// SwresTools +// + +import Foundation + +enum SeekableReaderError: SwresError { + case invalidLocation(Int) + case invalidRange(location: Int, length: Int) + case invalidParameter(name: String, value: Any) + case internalError + + var description: String { + switch(self) { + case .invalidLocation(let location): + return String.init(format: "Invalid seek to location 0x%x.", location) + case .invalidRange(let location, let length): + return String.init(format: "Invalid read at location 0x%x with length %d.", location, length) + case .invalidParameter: + return "Program error." + case .internalError: + return "Internal error." + } + } +} + +// Assumes big-endian byte order. +struct SeekableReader { + private let _data: Data + private var _offset: Int + + init(_ data: Data) { + _data = data + _offset = 0 + } + + private mutating func _readUInt32(length: Int) throws -> UInt32 { + guard length > 0 else { + assertionFailure() + throw SeekableReaderError.internalError + } + guard _offset + length <= _data.count else { + throw SeekableReaderError.invalidRange(location: _offset, length: length) + } + + var value: UInt32 = 0 + for _ in 1...length { + let byte = _data[_offset] + value = (value << 8) + UInt32(byte) + _offset += 1 + } + return value + } + + mutating func readInt8() throws -> Int8 { + return try Int8(truncatingBitPattern: _readUInt32(length: 1)) + } + + mutating func readInt16() throws -> Int16 { + return try Int16(truncatingBitPattern: _readUInt32(length: 2)) + } + + mutating func readInt24() throws -> Int32 { + return try Int32(bitPattern: _readUInt32(length: 3)) + } + + mutating func readInt32() throws -> Int32 { + return try Int32(bitPattern: _readUInt32(length: 4)) + } + + mutating func readBytes(_ length: Int) throws -> Data { + guard length > 0 else { + throw SeekableReaderError.invalidParameter(name: "length", value: length) + } + guard _offset + length <= _data.count else { + throw SeekableReaderError.invalidRange(location: _offset, length: length) + } + + let subdata = _data.subdata(in: _offset..<(_offset + length)) + _offset += length + return subdata + } + + mutating func readBytes(_ length: Int32) throws -> Data { + return try readBytes(Int(length)) + } + + mutating func seek(_ offset: Int) throws { + guard offset >= 0 && offset < _data.count else { + throw SeekableReaderError.invalidLocation(offset) + } + _offset = offset + } + + mutating func seek(_ offset: Int32) throws { + try seek(Int(offset)) + } + + mutating func skip(_ offset: Int) throws { + try(seek(_offset + offset)) + } +} diff --git a/SwresTools/Writer.swift b/SwresTools/Writer.swift new file mode 100644 index 0000000..621b87a --- /dev/null +++ b/SwresTools/Writer.swift @@ -0,0 +1,125 @@ +// +// Writer.swift +// SwresTools +// + +import Foundation + +enum WriterEndianness { + case littleEndian + case bigEndian +} + +enum WriterError: SwresError { + case invalidParameter(message: String) + case alreadyFinalized + case internalError + + var description: String { + switch self { + case .invalidParameter(let message): + return "Invalid parameter. \(message)" + case .alreadyFinalized: + return "The writer has already been finalized." + case .internalError: + return "Internal error." + } + } +} + +class Writer { + private let _endianness: WriterEndianness + private var _outputStream: OutputStream + private var _finalized: Bool = false + + init(endianness: WriterEndianness) { + _endianness = endianness + _outputStream = OutputStream.toMemory() + _outputStream.open() + } + + private func _unwrapped(_ endianness: WriterEndianness?) -> WriterEndianness { + if let unwrappedEndianness = endianness { + return unwrappedEndianness + } + return _endianness + } + + private func _validateNotFinalized() throws { + guard !_finalized else { + throw WriterError.alreadyFinalized + } + } + + func write(_ value: Int16, endianness: WriterEndianness? = nil) throws { + try _validateNotFinalized() + + var value = value + + switch _unwrapped(endianness) { + case .littleEndian: + value = value.littleEndian + case .bigEndian: + value = value.bigEndian + } + + try withUnsafePointer(to: &value) { (valuePointer: UnsafePointer) in + let bytePointer = UnsafeRawPointer(valuePointer).assumingMemoryBound(to: UInt8.self) + try _write(bytePointer, length: MemoryLayout.size) + } + } + + func write(_ value: Int32, endianness: WriterEndianness? = nil) throws { + try _validateNotFinalized() + + var value = value + + switch _unwrapped(endianness) { + case .littleEndian: + value = value.littleEndian + case .bigEndian: + value = value.bigEndian + } + + try withUnsafePointer(to: &value) { (valuePointer: UnsafePointer) in + let bytePointer = UnsafeRawPointer(valuePointer).assumingMemoryBound(to: UInt8.self) + try _write(bytePointer, length: MemoryLayout.size) + } + } + + func write(_ data: Data) throws { + try _validateNotFinalized() + + _ = try data.withUnsafeBytes { (pointer: UnsafePointer) in + try _write(pointer, length: data.count) + } + } + + func write(ascii: String) throws { + try _validateNotFinalized() + + guard let data = ascii.data(using: String.Encoding.ascii, allowLossyConversion: true) else { + throw WriterError.invalidParameter(message: "Invalid ASCII string.") + } + + try write(data) + } + + private func _write(_ pointer: UnsafePointer, length: Int) throws { + let writtenBytes = _outputStream.write(pointer, maxLength: length) + guard writtenBytes == length else { + throw WriterError.internalError + } + } + + func finalize() throws -> Data { + try _validateNotFinalized() + + let result = _outputStream.property(forKey: Stream.PropertyKey.dataWrittenToMemoryStreamKey) + guard let unwrappedResult = result, let data = unwrappedResult as? Data else { + throw WriterError.internalError + } + _finalized = true + return data + } +} diff --git a/Translators/PascalStringTranslator.swift b/Translators/PascalStringTranslator.swift new file mode 100644 index 0000000..acfb625 --- /dev/null +++ b/Translators/PascalStringTranslator.swift @@ -0,0 +1,49 @@ +// +// PascalStringTranslator.swift +// SwresTools +// + +import Foundation + +struct PascalStringTranslator: Translator { + @discardableResult func _looksLikePascalString(_ data: Data) throws -> Bool { + guard data.count > 0 else { + throw TranslatorError.unsupportedResource(reason: "Can't translate empty data.") + } + + let length = data[0] + guard data.count == Int(length) + 1 else { + throw TranslatorError.unsupportedResource(reason: "Resource length doesn't match first byte prefix.") + } + + return true + } + + static private var _strType: FourCharCode = try! FourCharCode("STR ") + + func compatibilityWith(resource: Resource) -> TranslatorCompatibility { + do { + try _looksLikePascalString(resource.data) + } catch { + return TranslatorCompatibility.notCompatible + } + + switch resource.type { + case PascalStringTranslator._strType: + return TranslatorCompatibility.likelyCompatible + default: + return TranslatorCompatibility.possiblyCompatible + } + } + + func translate(resource: Resource) throws -> Translation { + let data = resource.data + try _looksLikePascalString(data) + + let options = MacOSRomanConversionOptions(filterControlCharacters: false, filterFilesystemUnsafeCharacters: false, filterNonASCIICharacters: false, replacementMacOSRomanByte: nil) + let string = stringFromMacOSRomanBytes(data.subdata(in: 1.. TranslatorCompatibility { + if resource.type == SndResourceType { + return TranslatorCompatibility.likelyCompatible + } + return TranslatorCompatibility.notCompatible + } + + func translate(resource: Resource) throws -> Translation { + let reader = SeekableReader(resource.data) + + do { + let sndResource = try _readSndResource(reader) + let data = try _wavData(sndResource) + return Translation(data: data, suggestedFileExtension: SuggestedFileExtension) + } catch let error { + throw TranslatorError.invalidResource(reason: "Failed to parse snd resource.", underlyingError: error) + } + } + + private func _readSndResource(_ reader: SeekableReader) throws -> SndResource { + var reader = reader + let format = try reader.readInt16() + + guard format == 1 || format == 2 else { + throw TranslatorError.invalidResource(reason: "Unknown snd format \(format)", underlyingError: nil) + } + guard format == 1 else { + throw TranslatorError.unsupportedResource(reason: "Format 2 snd resources are not supported.") + } + + let dataFormatCount = try reader.readInt16() + guard dataFormatCount > 0 else { + throw TranslatorError.unsupportedResource(reason: "The resource doesn't contain any data formats.") + } + guard dataFormatCount == 1 else { + throw TranslatorError.unsupportedResource(reason: "The author hasn't read the spec closely enough to understand what to do when the resource contains more than one data format.") + } + + let dataType = try reader.readInt16() + guard dataType == 5 else { + throw TranslatorError.unsupportedResource(reason: "Only sampled sound data (type 0x0005) is supported.") + } + + // Skip the sound channel init options. + try reader.skip(4) + + let commandCount = try reader.readInt16() + guard commandCount == 1 else { + throw TranslatorError.unsupportedResource(reason: "The author hasn't read the spec closely enough to understand what to do when the resource contains more than one sound command.") + } + + let command = try reader.readInt16() + guard command == Int16(bitPattern: 0x8051) else { + throw TranslatorError.unsupportedResource(reason: "Only bufferCmd commands are supported.") + } + + // param1 seems to be unused for this command. + try reader.skip(2) + let soundHeaderOffset = try reader.readInt32() + + try reader.seek(soundHeaderOffset) + let sampleDataPointer = try reader.readInt32() + guard sampleDataPointer == 0 else { + throw TranslatorError.unsupportedResource(reason: "The author hasn't read the spec closely enough to understand what a non-zero sample data pointer means.") + } + + let sampleByteLength = try reader.readInt32() + guard sampleByteLength > 0 else { + throw TranslatorError.invalidResource(reason: "Sample has a negative length.", underlyingError: nil) + } + + // The sample rate is an unsigned 16.16 fixed point integer. Flooring the frequency loses + // information but the precision loss is less than one hundredth of one percent and WAV + // maybe doesn't support fractional frequencies? + let sampleRateFixed = try reader.readInt32() + let sampleRate = Int(sampleRateFixed >> 16) + + // Skip the two loop point parameters + try reader.skip(8) + + let sampleEncoding = try reader.readInt8() + guard sampleEncoding == 0 else { + throw TranslatorError.unsupportedResource(reason: "Encoded samples are not supported.") + } + + // Skip baseFrequency + try reader.skip(1) + + let sampleData = try reader.readBytes(sampleByteLength) + + return SndResource(sampleRate: sampleRate, data: sampleData) + } + + private func _wavData(_ sndResource: SndResource) throws -> Data { + let writer = Writer(endianness: .littleEndian) + + let data = sndResource.data + let dataCount = data.count + let wavFileSize = dataCount + WAVHeaderCount + + let sampleRate = sndResource.sampleRate + + // The header ChunkSize does not include the first ChunkID and size, i.e. + // it's the file size minus the first 8 bytes + try writer.write(ascii: "RIFF") + try writer.write(Int32(wavFileSize - 8)) + try writer.write(ascii: "WAVE") + + try writer.write(ascii: "fmt ") + try writer.write(Int32(16)) // Subchunk size + try writer.write(Int16(1)) // AudioFormat: PCM + try writer.write(Int16(1)) // Channel count + try writer.write(Int32(sampleRate)) + try writer.write(Int32(sampleRate)) // Bytes per second + try writer.write(Int16(1)) // Block alignment + try writer.write(Int16(8)) // Bits per sample + + try writer.write(ascii: "data") + try writer.write(Int32(dataCount)) + try writer.write(data) + + return try writer.finalize() + } +} diff --git a/Translators/Translator.swift b/Translators/Translator.swift new file mode 100644 index 0000000..8c8e25c --- /dev/null +++ b/Translators/Translator.swift @@ -0,0 +1,49 @@ +// +// Translator.swift +// SwresTools +// + +import Foundation + +enum TranslatorError: NestingSwresError { + case unsupportedResource(reason: String) + case invalidResource(reason: String?, underlyingError: Error?) + + var description: String { + switch self { + case .unsupportedResource(let reason): + return "The resource may be valid but this translator is not able to convert it. \(reason)" + case .invalidResource(let reason, _): + var message = "The resource does not appear to be valid." + if let unwrappedReason = reason { + message += " \(unwrappedReason)" + } + return message + } + } + + var underlyingError: Error? { + switch self { + case .invalidResource(_, let error): + return error + default: + return nil + } + } +} + +enum TranslatorCompatibility { + case notCompatible + case possiblyCompatible + case likelyCompatible +} + +struct Translation { + let data: Data + let suggestedFileExtension: String +} + +protocol Translator { + func compatibilityWith(resource: Resource) -> TranslatorCompatibility + func translate(resource: Resource) throws -> Translation +} diff --git a/Translators/TranslatorManager.swift b/Translators/TranslatorManager.swift new file mode 100644 index 0000000..1d3d291 --- /dev/null +++ b/Translators/TranslatorManager.swift @@ -0,0 +1,62 @@ +// +// TranslatorManager +// SwresTools +// + +import Foundation + +enum TranslatorFilter: Int, Comparable { + case noTranslators + case onlyLikelyTranslators + case likelyAndPossibleTranslators + + static func <(lhs: TranslatorFilter, rhs: TranslatorFilter) -> Bool { + return lhs.rawValue < rhs.rawValue + } +} + +enum TranslationResult { + case translated(_: Translation) + case error(_: Error) +} + +struct TranslatorManager { + static let sharedInstance = TranslatorManager() + + private let _translators: Array + + private init() { + _translators = [ + PascalStringTranslator(), + SndTranslator(), + ] + } + + func translate(_ resource: Resource, includeTranslators translatorFilter: TranslatorFilter = TranslatorFilter.noTranslators) -> Array { + var translationResults = Array() + + let translatorsByCompatibility = _translators.groupBy({ (translator: Translator) in + return translator.compatibilityWith(resource: resource) + }) + + var applicableTranslators = Array() + if let likelyTranslators = translatorsByCompatibility[TranslatorCompatibility.likelyCompatible] { + applicableTranslators.append(contentsOf: likelyTranslators) + } + + if translatorFilter >= TranslatorFilter.likelyAndPossibleTranslators, let possibleTranslators = translatorsByCompatibility[TranslatorCompatibility.possiblyCompatible] { + applicableTranslators.append(contentsOf: possibleTranslators) + } + + for translator in applicableTranslators { + do { + let translation = try translator.translate(resource: resource) + translationResults.append(TranslationResult.translated(translation)) + } catch let error { + translationResults.append(TranslationResult.error(error)) + } + } + + return translationResults + } +}