13

I am trying replicate some Objective C cocoa in Swift. All is good until I come across the following:

// Set a new type and creator:
unsigned long type = 'TEXT';
unsigned long creator = 'pdos';

How can I create Int64s (or the correct Swift equivalent) from single quote character literals like this?

Types:

public typealias AEKeyword = FourCharCode
public typealias OSType = FourCharCode
public typealias FourCharCode = UInt32
pkamb
  • 33,281
  • 23
  • 160
  • 191
James Alvarez
  • 7,159
  • 6
  • 31
  • 46

7 Answers7

10

I'm using this in my Cocoa Scripting apps, it considers characters > 0x80 correctly

func OSTypeFrom(string : String) -> UInt {
  var result : UInt = 0
  if let data = string.dataUsingEncoding(NSMacOSRomanStringEncoding) {
    let bytes = UnsafePointer<UInt8>(data.bytes)
    for i in 0..<data.length {
      result = result << 8 + UInt(bytes[i])
    }
  }
  return result
}

Edit:

Alternatively

func fourCharCodeFrom(string : String) -> FourCharCode
{
  assert(string.count == 4, "String length must be 4")
  var result : FourCharCode = 0
  for char in string.utf16 {
    result = (result << 8) + FourCharCode(char)
  }
  return result
}

or still swiftier

func fourCharCode(from string : String) -> FourCharCode
{
  return string.utf16.reduce(0, {$0 << 8 + FourCharCode($1)})
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • `OSTypeFrom` fails on strings longer than 4 characters, meantime macOS built-in `NSHFSTypeCodeFromFileType` returns 0 `fourCharCode` converts characters without encoding, e.g. it results `fourCharCode(from: "€©ß")==0x1FACA9DF` while it should be `NSHFSTypeCodeFromFileType("'€©ß'")) == 0xF0DBA9A7` – Paul Zabelin Feb 23 '20 at 22:29
6

I found the following typealiases from the Swift API:

typealias FourCharCode = UInt32
typealias OSType = FourCharCode

And the following functions:

func NSFileTypeForHFSTypeCode(hfsFileTypeCode: OSType) -> String!
func NSHFSTypeCodeFromFileType(fileTypeString: String!) -> OSType

This should allow me to create the equivalent code:

let type : UInt32 = UInt32(NSHFSTypeCodeFromFileType("TEXT"))
let creator : UInt32 = UInt32(NSHFSTypeCodeFromFileType("pdos"))

But those 4-character strings doesn't work and return 0.

If you wrap each string in ' single quotes ' and call the same functions, you will get the correct return values:

let type : UInt32 = UInt32(NSHFSTypeCodeFromFileType("'TEXT'"))
let creator : UInt32 = UInt32(NSHFSTypeCodeFromFileType("'pdos'"))
pkamb
  • 33,281
  • 23
  • 160
  • 191
James Alvarez
  • 7,159
  • 6
  • 31
  • 46
4

Adopt the ExpressibleByStringLiteral protocol to use four-character string literals directly:

extension FourCharCode: ExpressibleByStringLiteral {
    
    public init(stringLiteral value: StringLiteralType) {
        if let data = value.data(using: .macOSRoman), data.count == 4 {
            self = data.reduce(0, {$0 << 8 + Self($1)})
        } else {
            self = 0
        }
    }
   
}

Now you can just pass a string literal as the FourCharCode / OSType / UInt32 parameter:

let record = NSAppleEventDescriptor.record()
record.setDescriptor(NSAppleEventDescriptor(boolean: true), forKeyword: "test")
pkamb
  • 33,281
  • 23
  • 160
  • 191
2

In Swift 4 or later, I use this code - if the string is not 4 characters in size, it will return an OSType(0):

extension String {
    public func osType() -> OSType {
       var result:UInt = 0

       if let data = self.data(using: .macOSRoman), data.count == 4
       {
            data.withUnsafeBytes { (ptr:UnsafePointer<UInt8>) in
                for i in 0..<data.count {
                    result = result << 8 + UInt(ptr[i])
                }
            }
       }

       return OSType(result)
    }
}

let type = "APPL".osType()                 // 1095782476

// check if this is OK in a playground
let hexStr = String(format: "0x%lx", type) // 0x4150504c -> "APPL" in ASCII
jvarela
  • 3,744
  • 1
  • 22
  • 43
  • 2
    NB: Using `.macOSRoman` — as is done here — is correct. This matters for certain codes like `ƒhlp`. – Wevah Mar 08 '20 at 18:33
1

Swift 5 Update:

extension String {
    func osType() -> OSType {
        return OSType(
            data(using: .macOSRoman)?
                .withUnsafeBytes {
                    $0.reduce(into: UInt(0)) { $0 = $0 << 8 + UInt($1) }
                } ?? 0
        )
    }
}
0

Here's a simple function

func mbcc(foo: String) -> Int
{
    let chars = foo.utf8
    var result: Int = 0
    for aChar in chars
    {
        result = result << 8 + Int(aChar)
    }
    return result
}

let a = mbcc("TEXT")

print(String(format: "0x%lx", a)) // Prints 0x54455854

It will work for strings that will fit in an Int. Once they get longer it starts losing digits from the top.

If you use

result = result * 256 + Int(aChar)

you should get a crash when the string gets too big instead.

JeremyP
  • 84,577
  • 15
  • 123
  • 161
0

Using NSHFSTypeCodeFromFileType does work, but only for 4-character strings wrapped with single quotes, aka 6-character strings. It returns 0 for unquoted 4-character strings.

So wrap your 4-character string in ' ' before passing it to the function:

extension FourCharCode: ExpressibleByStringLiteral {
    
    public init(stringLiteral value: StringLiteralType) {
        switch (value.count, value.first, value.last) {
        case (6, "'", "'"):
            self = NSHFSTypeCodeFromFileType(value)
        case (4, _, _):
            self = NSHFSTypeCodeFromFileType("'\(value)'")
        default:
            self = 0
        }
    }
    
}

Using the above extension, you can use 4-character or single-quoted 6-character string literals:

let record = NSAppleEventDescriptor.record()
record.setDescriptor(NSAppleEventDescriptor(boolean: true), forKeyword: "4444")
record.setDescriptor(NSAppleEventDescriptor(boolean: true), forKeyword: "'6666'")

It would be even better to limit the string literal to 4-character strings at compile time. That does not seem to currently be possible, but is being discussed for Swift here:

Allow for Compile-Time Checked Intervals for Parameters Expecting Literal Values

pkamb
  • 33,281
  • 23
  • 160
  • 191