Initial commit from private projects repository.

This commit is contained in:
Paul Knight 2017-03-14 16:42:00 -07:00
parent a5d55c35df
commit d96267259c
31 changed files with 2456 additions and 0 deletions

2
.gitignore vendored
View File

@ -63,3 +63,5 @@ fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
.DS_Store

View File

@ -0,0 +1,4 @@
#include "Project.xcconfig"
ONLY_ACTIVE_ARCH = YES
SWIFT_OPTIMIZATION_LEVEL = -Onone

View File

@ -0,0 +1,5 @@
SDKROOT = macosx
WARNING_CFLAGS = -Wall -Wextra
SWIFT_VERSION = 3.0
SWIFT_INCLUDE_PATHS = "${PROJECT_DIR}/Modules"

View File

@ -0,0 +1,5 @@
#include "Project.xcconfig"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) NDEBUG=1
SWIFT_OPTIMIZATION_LEVEL = -O

View File

@ -0,0 +1,18 @@
//
// DictionaryExtras.swift
// SwresTools
//
extension Dictionary {
init(_ elements: Array<Element>){
self.init()
for (key, value) in elements {
self[key] = value
}
}
func flatMap(transform: (Key, Value) -> (Key, Value)?) -> Dictionary<Key, Value> {
return Dictionary(self.flatMap(transform))
}
}

28
Extras/ErrorExtras.swift Normal file
View File

@ -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
}
}
}

View File

@ -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<T> {
let pointer: UnsafePointer<T>
let count: Int
}
class ManagedUnsafeMutablePointer<T>: Hashable {
let pointer: UnsafeMutablePointer<T>
init(adoptPointer: UnsafeMutablePointer<T>) {
pointer = adoptPointer
}
deinit {
free(pointer)
}
var hashValue: Int {
return pointer.hashValue
}
static func ==(lhs: ManagedUnsafeMutablePointer, rhs: ManagedUnsafeMutablePointer) -> Bool {
return lhs.pointer == rhs.pointer
}
}

View File

@ -0,0 +1,22 @@
//
// SequenceExtras.swift
// SwresTools
//
extension Sequence {
func firstSome<ElementOfResult>(_ transform: @escaping (Iterator.Element) -> ElementOfResult?) -> ElementOfResult? {
return self.lazy.flatMap(transform).first
}
func groupBy<ElementOfResult: Hashable>(_ transform: (Iterator.Element) -> ElementOfResult) -> Dictionary<ElementOfResult, Array<Iterator.Element>> {
var groupedBy: Dictionary<ElementOfResult, Array<Iterator.Element>> = Dictionary()
for item in self {
let transformed = transform(item)
if groupedBy[transformed] == nil {
groupedBy[transformed] = Array<Iterator.Element>()
}
groupedBy[transformed]!.append(item)
}
return groupedBy
}
}

10
Extras/StrideExtras.swift Normal file
View File

@ -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))
}
}

100
Extras/StringExtras.swift Normal file
View File

@ -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<UInt8>, options: MacOSRomanConversionOptions) -> String {
let filterControlCharacters = options.filterControlCharacters || options.filterFilesystemUnsafeCharacters
let filteredBytes = UnsafeMutablePointer<UInt8>.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<UInt8>) 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<Int8> {
return self.withCString({ (cString: UnsafePointer<Int8>) -> ManagedUnsafeMutablePointer<Int8> in
return ManagedUnsafeMutablePointer(adoptPointer: strdup(cString))
})
}
}
struct StringAndCString {
let string: String
let cString: ManagedUnsafeMutablePointer<CChar>
init(_ string: String) {
self.string = string
cString = string.copyCString()
}
}

View File

@ -0,0 +1,5 @@
module Fuse [system] {
header "/usr/local/include/osxfuse/fuse.h"
link "osxfuse"
export *
}

49
README Normal file
View File

@ -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 <code>snd </code> 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.

View File

@ -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<Resource>) -> (FourCharCode, Array<Resource>)? 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<String>()
data.withUnsafeBytes { (unsafeBytes: UnsafePointer<UInt8>) 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<CChar> = [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<CChar>.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<UInt8>, 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<CChar>.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..<lineLength {
if byteIndex % 2 == 0 && byteIndex > 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)
}

View File

@ -0,0 +1 @@
PRODUCT_NAME = SwresExplode

8
SwresExplode/main.swift Normal file
View File

@ -0,0 +1,8 @@
//
// main.swift
// SwresTools
//
import Darwin
exit(swresExplodeMain())

BIN
SwresFUSE.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

View File

@ -0,0 +1,113 @@
//
// FilesystemNode.swift
// SwresTools
//
import Foundation
enum FilesystemNode {
case folder(name: StringAndCString, children: Dictionary<String, FilesystemNode>)
case file(name: StringAndCString, data: Data)
var cStringName: ManagedUnsafeMutablePointer<Int8> {
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<FilesystemNode>) {
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<Int8>) -> 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<String>) -> 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
}
}
}

View File

@ -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

324
SwresFUSE/SwresFuse.swift Normal file
View File

@ -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<fuse_args>?
}
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<CommandLineOption>, _ block: (UnsafePointer<fuse_opt>) -> Void) {
var cStringPool = Array<ManagedUnsafeMutablePointer<Int8>>()
var fuseOptions = ContiguousArray<fuse_opt>()
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<fuse_opt>) 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<fuse_args>.size).assumingMemoryBound(to: fuse_args.self)
args.pointee.argc = CommandLine.argc
args.pointee.argv = CommandLine.unsafeArgv
args.pointee.allocated = 0
withFuseOptions(options, { (fuseOptions: UnsafePointer<fuse_opt>) in
let parseResult = fuse_opt_parse(args, &task, fuseOptions, { (context: UnsafeMutableRawPointer?, arg: UnsafePointer<Int8>?, key: Int32, args: UnsafeMutablePointer<fuse_args>?) -> 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<Int8>?, stbuf: UnsafeMutablePointer<stat>?) -> 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<Int8>?, buf: UnsafeMutableRawPointer?, filler: fuse_fill_dir_t?, offset: off_t, fi: UnsafeMutablePointer<fuse_file_info>?) -> 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<Int8>) {
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<Int8>?, fi: UnsafeMutablePointer<fuse_file_info>?) -> 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<Int8>?, buf: UnsafeMutablePointer<Int8>?, size: size_t, offset: off_t, fi: UnsafeMutablePointer<fuse_file_info>?) -> 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<Int8>) -> 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<Resource>) -> FilesystemNode in
let folderName = filesystemSafeString(type.bytes)
let children = resources.flatMap { (resource: Resource) -> Array<FilesystemNode> 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<FilesystemNode> {
var nodes = Array<FilesystemNode>()
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)
}

8
SwresFUSE/main.swift Normal file
View File

@ -0,0 +1,8 @@
//
// main.swift
// SwresFUSE
//
import Darwin
exit(swresFuseMain())

View File

@ -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 = "<group>"; };
CA4150E41DAF4401005F689D /* SequenceExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SequenceExtras.swift; path = Extras/SequenceExtras.swift; sourceTree = "<group>"; };
CA4150E51DAF4401005F689D /* DictionaryExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DictionaryExtras.swift; path = Extras/DictionaryExtras.swift; sourceTree = "<group>"; };
CA4150E61DAF4401005F689D /* ErrorExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ErrorExtras.swift; path = Extras/ErrorExtras.swift; sourceTree = "<group>"; };
CA4150E71DAF4401005F689D /* PointerExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PointerExtras.swift; path = Extras/PointerExtras.swift; sourceTree = "<group>"; };
CA4150E81DAF4401005F689D /* StrideExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StrideExtras.swift; path = Extras/StrideExtras.swift; sourceTree = "<group>"; };
CA4150E91DAF4401005F689D /* StringExtras.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StringExtras.swift; path = Extras/StringExtras.swift; sourceTree = "<group>"; };
CA4150F01DAF4407005F689D /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = SwresExplode/main.swift; sourceTree = "<group>"; };
CA4150F11DAF4407005F689D /* SwresExplode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwresExplode.swift; path = SwresExplode/SwresExplode.swift; sourceTree = "<group>"; };
CA4150F41DAF440F005F689D /* PascalStringTranslator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PascalStringTranslator.swift; path = Translators/PascalStringTranslator.swift; sourceTree = "<group>"; };
CA4150F51DAF440F005F689D /* SndTranslator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SndTranslator.swift; path = Translators/SndTranslator.swift; sourceTree = "<group>"; };
CA4EB2DF1DB9E44300A775DF /* FilesystemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesystemNode.swift; sourceTree = "<group>"; };
CA572C501DB2058E00A2AF8F /* Writer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Writer.swift; sourceTree = "<group>"; };
CA584C8C1DB47C9300E36C26 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; name = module.modulemap; path = Modules/FUSE/module.modulemap; sourceTree = "<group>"; };
CA584C8F1DB4894500E36C26 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Configurations/Debug.xcconfig; sourceTree = "<group>"; };
CA584C901DB4894500E36C26 /* Project.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Project.xcconfig; path = Configurations/Project.xcconfig; sourceTree = "<group>"; };
CA584C911DB4894500E36C26 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Configurations/Release.xcconfig; sourceTree = "<group>"; };
CA584C941DB4897800E36C26 /* SwresExplode.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = SwresExplode.xcconfig; path = SwresExplode/SwresExplode.xcconfig; sourceTree = "<group>"; };
CA584C951DB4897F00E36C26 /* SwresFUSE.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SwresFUSE.xcconfig; sourceTree = "<group>"; };
CA584C961DB491DF00E36C26 /* SwresFuse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwresFuse.swift; sourceTree = "<group>"; };
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 = "<group>"; };
CA6136641DA4ADD0001F81DA /* Translator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Translator.swift; path = Translators/Translator.swift; sourceTree = "<group>"; };
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 = "<group>"; };
CA9007CE1D8DBCAF00B7D2BD /* ResourceForkReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceForkReader.swift; sourceTree = "<group>"; };
CA9007D01D8DBD1900B7D2BD /* FourCharCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FourCharCode.swift; sourceTree = "<group>"; };
CA9007D41D8DF9C400B7D2BD /* Resource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
CA584C8D1DB47C9F00E36C26 /* Modules */ = {
isa = PBXGroup;
children = (
CA5AA8F01DB44E40001866EB /* FUSE */,
);
name = Modules;
sourceTree = "<group>";
};
CA584C8E1DB4893300E36C26 /* Configurations */ = {
isa = PBXGroup;
children = (
CA584C8F1DB4894500E36C26 /* Debug.xcconfig */,
CA584C901DB4894500E36C26 /* Project.xcconfig */,
CA584C911DB4894500E36C26 /* Release.xcconfig */,
);
name = Configurations;
sourceTree = "<group>";
};
CA5AA8EA1DB44814001866EB /* SwresFUSE */ = {
isa = PBXGroup;
children = (
CA5AA8EB1DB44814001866EB /* main.swift */,
CA4EB2DF1DB9E44300A775DF /* FilesystemNode.swift */,
CA584C961DB491DF00E36C26 /* SwresFuse.swift */,
CA584C951DB4897F00E36C26 /* SwresFUSE.xcconfig */,
);
path = SwresFUSE;
sourceTree = "<group>";
};
CA5AA8F01DB44E40001866EB /* FUSE */ = {
isa = PBXGroup;
children = (
CA584C8C1DB47C9300E36C26 /* module.modulemap */,
);
name = FUSE;
sourceTree = "<group>";
};
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 = "<group>";
};
CA6136611DA4ADA2001F81DA /* Translators */ = {
isa = PBXGroup;
children = (
CA4150F41DAF440F005F689D /* PascalStringTranslator.swift */,
CA4150F51DAF440F005F689D /* SndTranslator.swift */,
CA6136641DA4ADD0001F81DA /* Translator.swift */,
CA2B20FE1DA57DAB00A14B92 /* TranslatorManager.swift */,
);
name = Translators;
sourceTree = "<group>";
};
CA9007B91D8DAFB500B7D2BD = {
isa = PBXGroup;
children = (
CA6136601DA49D48001F81DA /* Extras */,
CA9007C41D8DAFB500B7D2BD /* SwresTools */,
CA6136611DA4ADA2001F81DA /* Translators */,
CA4150E31DAF439C005F689D /* SwresExpolode */,
CA5AA8EA1DB44814001866EB /* SwresFUSE */,
CA584C8D1DB47C9F00E36C26 /* Modules */,
CA584C8E1DB4893300E36C26 /* Configurations */,
CA9007C31D8DAFB500B7D2BD /* Products */,
);
sourceTree = "<group>";
};
CA9007C31D8DAFB500B7D2BD /* Products */ = {
isa = PBXGroup;
children = (
CA9007C21D8DAFB500B7D2BD /* SwresExplode */,
CA5AA8E91DB44814001866EB /* SwresFUSE */,
);
name = Products;
sourceTree = "<group>";
};
CA9007C41D8DAFB500B7D2BD /* SwresTools */ = {
isa = PBXGroup;
children = (
CA9007D01D8DBD1900B7D2BD /* FourCharCode.swift */,
CA9007D41D8DF9C400B7D2BD /* Resource.swift */,
CA9007CE1D8DBCAF00B7D2BD /* ResourceForkReader.swift */,
CA9007CC1D8DB31400B7D2BD /* SeekableReader.swift */,
CA572C501DB2058E00A2AF8F /* Writer.swift */,
);
path = SwresTools;
sourceTree = "<group>";
};
/* 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:SwresTools.xcodeproj">
</FileRef>
</Workspace>

View File

@ -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<CChar>) 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)
}
}

27
SwresTools/Resource.swift Normal file
View File

@ -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 "<Resource type: \"\(type)\", identifier: \(identifier), name: \"\(formattedName)\", data length: \(data.count)>"
}
}

View File

@ -0,0 +1,189 @@
//
// ResourceManager.swift
// SwresTools
//
// Resource Map documentation comes from Inside Macintosh: More Macintosh Toolbox (1993).
import Foundation
typealias ResourcesByType = Dictionary<FourCharCode, Array<Resource>>
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<Resource> {
var reader = reader
var resources = Array<Resource>()
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)
}

View File

@ -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))
}
}

125
SwresTools/Writer.swift Normal file
View File

@ -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<Int16>) in
let bytePointer = UnsafeRawPointer(valuePointer).assumingMemoryBound(to: UInt8.self)
try _write(bytePointer, length: MemoryLayout<UInt16>.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<Int32>) in
let bytePointer = UnsafeRawPointer(valuePointer).assumingMemoryBound(to: UInt8.self)
try _write(bytePointer, length: MemoryLayout<UInt32>.size)
}
}
func write(_ data: Data) throws {
try _validateNotFinalized()
_ = try data.withUnsafeBytes { (pointer: UnsafePointer<UInt8>) 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<UInt8>, 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
}
}

View File

@ -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..<data.count), options: options)
let stringData = string.data(using: String.Encoding.utf8)
return Translation(data: stringData!, suggestedFileExtension: "txt")
}
}

View File

@ -0,0 +1,149 @@
//
// SndTranslator.swift
// SwresTools
//
// This is an incredibly basic converter for `Snd ' resources. It does not attempt to actually
// parse headers or commands property. It does not recognize extended headers. It does not
// do anything useful for anything other than the most basic, 8-bit mono PCM samples with
// super boring and obvious headers.
// Resournce information comes from Inside Macintosh (1993).
import Foundation
private let SndResourceType: FourCharCode = try! FourCharCode("snd ")
private let SuggestedFileExtension: String = "wav"
private let WAVHeaderCount = 44
private struct SndResource {
let sampleRate: Int
let data: Data
}
struct SndTranslator: Translator {
func compatibilityWith(resource: Resource) -> 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()
}
}

View File

@ -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
}

View File

@ -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<Translator>
private init() {
_translators = [
PascalStringTranslator(),
SndTranslator(),
]
}
func translate(_ resource: Resource, includeTranslators translatorFilter: TranslatorFilter = TranslatorFilter.noTranslators) -> Array<TranslationResult> {
var translationResults = Array<TranslationResult>()
let translatorsByCompatibility = _translators.groupBy({ (translator: Translator) in
return translator.compatibilityWith(resource: resource)
})
var applicableTranslators = Array<Translator>()
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
}
}