SecureCoding roadblock?

Attached is an entire project (4 files) that mirrors my actual project including the failure to save to file. Am I:

  1. missing some syntax in this code?
  2. failing to config a defaults file?
  3. not set the necessary parameters in " "Build Settings" or "Build Rules etc.?

I was writing to JSON files, but now that I must append to files directly, and JSON doesn't do that easily, I am trying to write using native macOS tools.

WELL, IT SEEMS I CAN'T SEND YOU THE CODE, TOO MANY CHARS. I CAN'T ATTACH ANY FILE EITHER. WHY OFFER IT IF IT IS NOT ALLOWED? ANYWAY, CAN YOU GLEAN ANYTHING FROM THIS... Thanks.

My debugger area: 2022-05-28 12:03:11.827372-0500 exampleClassInClassSecureCoding[1508:29981] Metal API Validation Enabled 2022-05-28 12:03:11.940123-0500 exampleClassInClassSecureCoding[1508:29981] *** NSForwarding: warning: object 0x600003cf7090 of class 'exampleClassInClassSecureCoding.classOne' does not implement methodSignatureForSelector: -- trouble ahead Unrecognized selector -[exampleClassInClassSecureCoding.classOne replacementObjectForKeyedArchiver:] 2022-05-28 12:03:11.940416-0500 exampleClassInClassSecureCoding[1508:29981] Unrecognized selector -[exampleClassInClassSecureCoding.classOne replacementObjectForKeyedArchiver:] Unrecognized selector -[exampleClassInClassSecureCoding.classOne replacementObjectForKeyedArchiver:] Performing @selector(didPressButton:) from sender _TtC7SwiftUIP33_9FEBA96B0BC70E1682E82D239F242E7319SwiftUIAppKitButton 0x7ff08ab06480

Accepted Reply

Is there no way to to append-to-file piecemeal and then read it again?

You can absolutely do that, but you need to use a file format that supports it. The two file formats you’ve referenced, JSON and keyed archives, don’t.

If being keyed is the roadblock

No. The same restriction applies to un-keyed archives.

my original app stored as JSON text anyway

One trick commonly used by JSON folks is to use line-based JSON. That is render your JSON to a single line and then use line endings as a delimiter. For the details, see ndjson http://ndjson.org/. You can read such files by reading one line at a time and parsing each line as a separate JSON structure.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

Sorry, I'll try that again. Here are the class declarations. I suspect my problem is in them.

class classOne: Equatable, NSSecureCoding, Codable, NSCoding {
    
    static var supportsSecureCoding = true
    
    var a_One :  String
    var b_One : [UInt8]
    
    init(a_One: String?, b_One: [UInt8]?)
    {
        self.a_One = a_One ??  String()
        self.b_One = b_One ?? [UInt8]()
    }
    
    enum CodingKeys: String, CodingKey { case a_One, b_One }
    static var allowedTopLevelClasses: [AnyClass] {
        return [NSArray.self, NSString.self]
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(a_One, forKey: CodingKeys.a_One.rawValue)
        aCoder.encode(b_One, forKey: CodingKeys.b_One.rawValue)
    }
    required init?(coder aDecoder: NSCoder) {
        a_One = aDecoder.decodeObject(forKey: CodingKeys.a_One.rawValue) as?  String ??  String()
        b_One = aDecoder.decodeObject(forKey: CodingKeys.b_One.rawValue) as? [UInt8] ?? [UInt8]()
    }
    
    static func == (lhs: classOne, rhs: classOne) -> Bool { // unnecessary but mirrors my more-elaborate code
        if lhs.a_One == rhs.a_One && lhs.b_One == rhs.b_One { return true }
        else { return false }
    }
}


class classTwo: NSObject, NSSecureCoding, Codable, NSCoding {
    static var supportsSecureCoding = true
    
    var c_Two : Int
    var d_Two : Double
    var e_Two : [classOne]
    
    init(c_Two: Int, d_Two: Double?, e_Two: [classOne]? ) {
        self.c_Two = c_Two
        self.d_Two = d_Two ?? Double()
        self.e_Two = e_Two ?? [classOne]()
    }
    
    enum CodingKeys: String, CodingKey { case c_Two, d_Two, e_Two }
    static var allowedTopLevelClasses: [AnyClass] { return [NSArray.self, NSString.self, classOne.self] }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(c_Two, forKey: CodingKeys.c_Two.rawValue)
        aCoder.encode(d_Two, forKey: CodingKeys.d_Two.rawValue)
        aCoder.encode(e_Two, forKey: CodingKeys.e_Two.rawValue)
    }
    
    required init(coder aDecoder: NSCoder) {
        c_Two = aDecoder.decodeInteger(forKey: CodingKeys.c_Two.rawValue)
        d_Two = aDecoder.decodeDouble(forKey: CodingKeys.d_Two.rawValue)
        e_Two = aDecoder.decodeObject(forKey: CodingKeys.e_Two.rawValue) as? [classOne] ?? [classOne]()
    }
}

  • the runtime aborts when it hits a classTwo.init() @ self.e_Two = e_Two ?? [class...

  • NO. runtime actually aborts @ aCoder.encode(e_Two, forKey: CodingKeys.e_Two.rawValue) during a save-to-file function.

Add a Comment

piecemeal then:

func genData() {
    var myOutput = Array(repeating: [classOne](), count: 16 )
    let words:[UInt8] = [ 0b01111100,
                          0b00111100,
                          0b00011100,
                          0b00001100,
                          0b00001000,
                          0b00000000 ]
    
    for word1 in words {
        for word2 in words {
            for word3 in words {
                
                let candidate = classOne(
                    a_One: "Nothing",
                    b_One: [word1, word2, word3]
                )
                let bitCount = word1.nonzeroBitCount + word2.nonzeroBitCount + word3.nonzeroBitCount
                myOutput[bitCount].append(candidate)
            }
        }
    }
    let dataToSave = classTwo(c_Two: Int(myOutput[15][0].b_One[0]), d_Two: log(Double(myOutput[15][0].b_One.count)), e_Two: myOutput[15]) // just some/any data to save
    saveFile(fileName: "TestFileSecureCodingExample", object: dataToSave, ext: "test", appendToFile: kCFBooleanFalse)
}

//
//  save-load-code.swift
//  exampleClassInClassSecureCoding
//
import Foundation

func saveFile(fileName: String, object: classTwo, ext: String, appendToFile: CFBoolean) {
    let fileDir = URL(fileURLWithPath: "~/")
    let fileURL = URL(fileURLWithPath: "~/\(fileName)").appendingPathExtension(ext)
    if !FileManager.default.fileExists(atPath: fileDir.path) {
        do { try FileManager.default.createDirectory(atPath: "~/", withIntermediateDirectories: false, attributes: nil) }
        catch { print(error) }
        
        do { let data = try NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: true)
            try data.write(to: fileURL )
            let defaults = UserDefaults.standard
            defaults.set(data, forKey: "classTwo")
        }
        catch { print(error) }
    }
    else if appendToFile == kCFBooleanTrue {
        do {
            let fileHandle  = try FileHandle(forWritingTo: fileURL )
            let data        = try NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: true)
            try fileHandle.seekToEnd()
            let buffer      = FileHandleBuffer(fileHandle: fileHandle)
            buffer.size     = data.count
            try buffer.write(data)
            let defaults = UserDefaults.standard
            defaults.set(data, forKey: "classTwo")
        }
        catch { print(error) }
    }
}



func loadFile(_ N: Int) -> classTwo? {
    /* https://developer.apple.com/forums/thread/115643 OOPer   https://www.hackingwithswift.com/read/12/3/fixing-project-10-nscoding */
    let fileURL  = URL(fileURLWithPath: "~/\" ).appendingPathExtension("gut")
    let defaults = UserDefaults.standard
    if let data  = defaults.object(forKey: "classTwo") as? Data {
        if FileManager.default.fileExists(atPath: fileURL.path) {
            do {
                if let BA = try? NSKeyedUnarchiver.unarchivedObject(ofClass: classTwo.self, from: data) {
                    return BA
                }
            }
        }
    }
    return nil
}

  • OK, that's all the unique code. If you cut and paste into a new project of your making, you can run it and get the same errors. Thanks.

Add a Comment

I’m confused by your overall goal here. You wrote:

I was writing to JSON files, but now that I must append to files directly, and JSON doesn't do that easily, I am trying to write using native macOS tools.

but the code you posted is based on keyed archiving. Keyed archiver doesn’t support append any more than JSON does.

In your saveFile(…) method you create some archived data and append it to the end of a file. Presumably that means that you want to add multiple archives to the file. And then in loadFile(…) you attempt to read it back as one archive. That won’t work. When you create a keyed archive the resulting data stands alone. You can’t merge the results of two archive operations.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

eskimo, thanks for your reply.

  1. Is there no way to to append-to-file piecemeal and then read it again?
  2. If being keyed is the roadblock, I really don't need it as my original app stored as JSON text anyway. So, how can I save without being keyed? My understanding was that Apple would soon require "SecureCoding" which in turn required keying.

Is there no way to to append-to-file piecemeal and then read it again?

You can absolutely do that, but you need to use a file format that supports it. The two file formats you’ve referenced, JSON and keyed archives, don’t.

If being keyed is the roadblock

No. The same restriction applies to un-keyed archives.

my original app stored as JSON text anyway

One trick commonly used by JSON folks is to use line-based JSON. That is render your JSON to a single line and then use line endings as a delimiter. For the details, see ndjson http://ndjson.org/. You can read such files by reading one line at a time and parsing each line as a separate JSON structure.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I looked into ndjson. It required a library download and had a number of conditions for which it would fail. It turns out that editing and appending to my JSON file was not hard at all, due to the consistency of the JSON encoder.

Since eskimo provided the invaluable info that keyed archives don't support appending, I'll credit him.

Thank you.

FYI, I append JSON files millions of times per file. Here's the code:

func saveBAToJSON(BA: myCustomClass) {
 let fileName   = "created from BA"
 let ArchiveURL = getArchiveURL(fileName: fileName, ext: "JSON")
 var ArchiveDirectoryPath = fileDirPath
 let localFileManager = FileManager()
 do {
  let resources = try? ArchiveURL.resourceValues(forKeys: [.fileSizeKey])
  let fileSize = resources?.fileSize ?? 0
  /* The JSON file is appended many times and it must be limited in size otherwise memory is over taxed. maxFileSize is a global var that is machine specific. */
  if fileSize > maxFileSize {
   let dirContents = try! localFileManager.contentsOfDirectory(atPath: ArchiveDirectoryPath)
   var elemFiles = 0
   /* If a file would exceed MaxFileSize, start a new file copying it and appending an incremented counter to the name. */
   for elem in 0..<dirContents.count { if dirContents[elem].contains(fileName) { elemFiles += 1 } }
   try FileManager.default.copyItem(at: ArchiveURL, to: getArchiveURL(fileName: fileName+"-\(elemFiles)", ext: "JSON"))
   try FileManager.default.removeItem(at: ArchiveURL)
  }
 } catch { print("\(Date.now):\(#file):\(#line) saveBAToJSON():fileSize", error) }
 do {
  let myJSONencoder = JSONEncoder()
  myJSONencoder.nonConformingFloatEncodingStrategy = .convertToString(positiveInfinity: "+Infinity", negativeInfinity: "-Infinity", nan: "NaN")
  var data = try myJSONencoder.encode(BA)
  if let fileHandle = try? FileHandle(forWritingTo: ArchiveURL) {
   defer {
    fileHandle.closeFile()
   }
   /* This is the main trick: truncate the repeating JSON components from the top of file to be appended, strip the JSON components at the end of the file to append-to (after converting to base64) replacing it with a comma (in my case).  Write to file.  */
   let truncateCount = UInt64(36 + filename.count + String(BA.myCustomClassElement1).count)
   /* you must count chars in your specific JSON's to get this right. I'm sure this could be coded to adapt to any JSON file. */
   let strippedRange = data.range(of: Data(base64Encoded: "my base64 encoded repeating components at end-of-file" )!)
   let commaData     = Data(base64Encoded: "LA==")!
   data.replaceSubrange(strippedRange!, with: commaData )
   let end           = try fileHandle.seekToEnd()
   if end > truncateCount {
    try fileHandle.truncate(atOffset: end - truncateCount )
    try fileHandle.write(contentsOf: data )
   }
  }
  else { try data.write(to: ArchiveURL, options: [.atomic, .noFileProtection]) }
 } catch { print("\(Date.now):\(#file):\(#line) saveBAToJSON():fileSize", error) }
}

Share and Enjoy homage Eskimo