crabstep 0.5.0

Cross-platform, zero-dependency Apple/NeXTSTEP typedstream deserializer
Documentation
// Generates the Foundation typedstream test fixtures in src/test_data/foundation/
// using the *classic* NSArchiver (the typedstream archiver). Run via generate.sh.
//
// One object is archived per process invocation: a class that refuses non-keyed
// archiving raises an NSException that aborts only this run (caught by the script
// as a nonzero exit), instead of taking down the whole batch.
//
// NSArchiver is deprecated but still emits `streamtyped` on current macOS. Only
// Tier-1 Foundation classes are generated here (see FOUNDATION_FEATURE_SCOPING.md);
// AppKit/Tier-2 classes are intentionally excluded.

import Foundation

func make(_ key: String) -> Any? {
    switch key {
    // string cluster
    case "NSString":            return NSString(string: "Hello, world")
    case "NSMutableString":     return NSMutableString(string: "Hello, world")
    case "NSAttributedString":  return NSAttributedString(string: "Hello, world")
    // data cluster
    case "NSData":              return NSData(bytes: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x7F] as [UInt8], length: 6)
    case "NSMutableData":       return NSMutableData(bytes: [0xDE, 0xAD, 0xBE, 0xEF] as [UInt8], length: 4)
    // numbers (exactly-representable values so tests can assert equality)
    case "NumberBool":          return NSNumber(value: true)
    case "NumberInt":           return NSNumber(value: Int32(42))
    case "NumberFloat":         return NSNumber(value: Float(3.5))
    case "NumberDouble":        return NSNumber(value: Double(100.5))
    case "NumberInt64":         return NSNumber(value: Int64(-9_000_000_000)) // exercises the 8-byte form
    // arrays
    case "NSArray":             return NSArray(array: [NSString(string: "a"), NSNumber(value: 1), NSString(string: "b")])
    case "NSMutableArray":      return NSMutableArray(array: [NSString(string: "x"), NSNumber(value: 2)])
    case "NSArrayEmpty":        return NSArray(array: [])
    case "NSArrayWithNull":     return NSArray(array: [NSString(string: "x"), NSNull(), NSString(string: "y")])
    case "NSArrayNested":       return NSArray(array: [NSString(string: "top"), NSArray(array: [NSNumber(value: 1), NSNumber(value: 2)])])
    // dictionaries
    case "NSDictionary":        return NSDictionary(objects: [NSNumber(value: 1), NSString(string: "v")], forKeys: [NSString(string: "k1"), NSString(string: "k2")])
    case "NSMutableDictionary":
        let d = NSMutableDictionary()
        d.setObject(NSNumber(value: 9), forKey: NSString(string: "key"))
        return d
    case "NSDictionaryNested":
        return NSDictionary(
            objects: [
                NSArray(array: [NSNumber(value: 1), NSNumber(value: 2)]),
                NSData(bytes: [0x01, 0x02] as [UInt8], length: 2),
                NSNumber(value: 7),
            ],
            forKeys: [NSString(string: "arr"), NSString(string: "data"), NSString(string: "num")]
        )
    // sets
    case "NSSet":               return NSSet(array: [NSString(string: "s1"), NSString(string: "s2")])
    case "NSMutableSet":        return NSMutableSet(array: [NSString(string: "m1")])
    // dates (integral dodges the DECIMAL path; fractional exercises it)
    case "NSDate":              return NSDate(timeIntervalSinceReferenceDate: 21692800)        // unix 1_000_000_000
    case "NSDateFractional":    return NSDate(timeIntervalSinceReferenceDate: 700000000.523)
    // urls
    case "NSURL":               return NSURL(string: "https://example.com/path?q=1")
    case "NSURLRelative":       return NSURL(string: "page.html", relativeTo: URL(string: "https://example.com/dir/"))
    // nested fixtures: accessors operate on objects wrapped in a group, so these
    // place each cluster variant as an array element (a root object's groups are
    // its contents, not a wrapper around it).
    case "NestedStrings":       return NSArray(array: [NSString(string: "imm"), NSMutableString(string: "mut")])
    case "NestedData":          return NSArray(array: [NSData(bytes: [0x01, 0x02] as [UInt8], length: 2), NSMutableData(bytes: [0x03, 0x04, 0x05] as [UInt8], length: 3)])
    case "NestedAttributed":    return NSArray(array: [NSAttributedString(string: "styled")])
    case "NestedContainers":
        // Nested containers (both cluster variants + an empty array) so the
        // container accessors can be tested on objects wrapped in a group.
        let md = NSMutableDictionary()
        md.setObject(NSNumber(value: 8), forKey: NSString(string: "mk"))
        return NSArray(array: [
            NSArray(array: [NSNumber(value: 1), NSNumber(value: 2)]),
            NSMutableArray(array: [NSNumber(value: 3)]),
            NSArray(array: []),
            NSDictionary(objects: [NSNumber(value: 9)], forKeys: [NSString(string: "k")]),
            md,
            NSSet(array: [NSString(string: "s")]),
            NSMutableSet(array: [NSString(string: "ms")]),
        ])
    case "NestedScalars":
        // NSDate / NSURL (absolute + relative) / NSNull as array elements, so the
        // Phase 4 accessors can be tested on objects wrapped in a group.
        return NSArray(array: [
            NSDate(timeIntervalSinceReferenceDate: 21692800), // unix 1_000_000_000
            NSURL(string: "https://example.com/path?q=1")!,
            NSURL(string: "page.html", relativeTo: URL(string: "https://example.com/dir/"))!,
            NSNull(),
        ])
    default:                    return nil
    }
}

let args = CommandLine.arguments
guard args.count >= 3, let obj = make(args[1]) else {
    FileHandle.standardError.write("skip \(args.count >= 2 ? args[1] : "?")\n".data(using: .utf8)!)
    exit(2)
}
let data = NSArchiver.archivedData(withRootObject: obj)
try! data.write(to: URL(fileURLWithPath: args[2]))
print("ok \(args[1]) -> \(data.count) bytes")