A deep dive into Apple's .car file format
151 points by MrFinch 4 days ago | 57 comments

wzdd 13 hours ago
The fact that different parts of the file use different endiannesses really added that special Apple tech flavour.
reply
nxobject 11 hours ago
The BOM file format is very, very old - I think for the first decade of its life it actually lived on big-endian platforms (m68k/SPARC/PA-RISC NeXTSTEP, and then over to Apple and big-endian PowerPC.)

EDIT - someone needs to crack open x86 OPENSTEP and do some grepping for 'RATC'...

reply
NSUserDefaults 12 hours ago
At this point if a file format could have rounded corners I’m sure it would too.
reply
1970-01-01 9 hours ago
"You're holding it wrong", but in binary.
reply
jitl 14 hours ago
Claude is pretty good at turning (dis)assembly into Objective-C. i went exploring these systems looking for the secrets of glass icon rendering. i used ipsw to unpack all the class metadata in relevant system private frameworks. for each class, i extracted class header/interface, and a assembly file per method in the header. i wrote a ruby script to shell out to claude cli with a custom system prompt to give me readable-ish obj-c. It struggled with some patterns but with code as string-dispatch method-call-heavy as obj-c there’s lots of good hints for the ai.

i learned a lot about lldb debugging when i went spelunking through system service process memory. eventually i got too distracted learning about runtime introspection in Swift and obj-c and ended up building a dynamic object explorer/debugger instead of accomplishing my original goal. obj-c runtime dynamism is fascinating. it’s like, “what if we make C as dynamic as Ruby”. you can invent new classes at runtime, swap method implementations, create a new class that extends a specific existing object. you can even change what class an object is.

Swift is a lot less dynamic and a lot less introspectable at runtime :-( (there is a swift reflection api called Mirror but i struggled to do anything interesting with it)

reply
JamesSwift 10 hours ago
> obj-c runtime dynamism is fascinating. it’s like, “what if we make C as dynamic as Ruby”.

Well thats because objc and ruby are cousins. Both are sort of the only two smalltalk based languages out there

reply
JadeNB 10 hours ago
> Well thats because objc and ruby are cousins. Both are sort of the only two smalltalk based languages out there

I'm sure you can trace connections, at least in ideas, but I think Ruby is way more Perl-based than Smalltalk-based.

reply
JamesSwift 10 hours ago
Im talking the fundamental language framework. 'Everything is an object' and method calls are actually message passing are the two reasons that objc and ruby are actually smalltalks.
reply
jonhohle 8 hours ago
I’ve tried to explain this before and unless you’re steeped in late binding, encapsulation, and message passing, the details are lost on most people (it seems including modern language designers).

For the GP, in most languages the dot or arrow operator is field access. If that field is a function reference, parenthesis are used to invoke it.

From outside of the object, neither Ruby or Objective-C allow direct access to object fields or functions. The dot operator sends the object a message that be bound to anything, and even rebound at runtime for specific instances. There is no difference between access and property and calling a function - it’s all messages. Smalltalk and Objective-C (before dot operators) don’t even have different syntax for data fields and functions calls. Ruby’s no arg messages are similar.

Most of the time that distinction doesn’t matter. But writing things like wrappers and proxies becomes trivial. A object can forward any message it receives, and if it sees on it wants to intercept, it can do that easily. Most of the time modifying existing programs and frameworks can be as easy as rebinding some logic to something that wasn’t part of the original program.

This comes at the cost of some runtime performance, and possibly some complexity. The elegance outweighs those, imho.

reply
crandyboy 10 hours ago
Team manager at NeXT worked on the file format here, AMA
reply
jonhohle 8 hours ago
Do you feel like the current Apple engineering orgs either don’t understand the philosophy that came from NeXT or has the world changed and some of those choices are no longer possible?

For example, until a decade ago, macOS was extremely scriptable and consistent. IB was flexible, but approachable. There were few specialized frameworks, but a lot was possible with just Cocoa. Now it seems like dynamism at the programming level is frowned upon, scripting and automation require constant user permission, Swift seems to favor performance over dynamism, and it seems like any new concepts are now all relegated to their own framework.

reply
giantrobot 6 hours ago
Not the GP but a lot of application level scriptability came from AppleScript/Apple Events which was a MacOS thing and not from NeXT. There was a ton of work that went into adding Apple Events support into AppKit (it was already baked into Carbon) which gave Cocoa applications a level of "free" scriptability. Same with UI scripting. Just adding AppleScript names, I forget the exact terminology now, to a UI control made it accessible via AppleScript.

That never existed in iOS and as UIKit was merged into macOS that old support was never added back in. AFAIK the Shortcuts system uses a totally different automation mechanism for scripting applications.

For a lot of applications, especially ones that started life in the classic MacOS days, AppleScript automation was pretty amazing. You could easily tie very disparate applications together.

The application level automation was orthogonal to the use of Objective-C or even Cocoa.

Source: I was at Apple from 2004-2020 and did a lot with automation over the years while there.

reply
mrexcess 5 hours ago
What was it really like working at NeXT? Was there always a sense that you were building what would ideally become Mac OS X, or was that plan held close to the vest? Any cool or memorable Steve Jobs stories? What was with the greyscale debut?
reply
jezek2 7 hours ago
Thanks, it might be useful for macOS support of non-native widgets in my GUI library. I did some parsing but got stuck at some point.

Seems a better choice to just parse the files than using private APIs as I couldn't see much of the documentation for them (or rather reversed APIs in various projects) + image handling is weirdly complicated in macOS generally so I want to avoid that.

Currently I've experimented by just recreating the graphics from scratch using vector rendering but it's quite cumbersome.

Other libraries, such as Qt, try to draw the native widgets into offscreen image but it's extremely hacky way prone to GUI breaking with any OS update. Java has their own Apple blessed way, but seems quite limited to me and don't want to depend on that either.

reply
mwkaufma 8 hours ago
Originally read "BOM (Bill of Materials)" and thought it was a misattribution of the more common "Byte Order Marker" acronym common to file format headers, but websurfing reveals it's correct -- referring to this format's origin in Installers, haha.
reply
jollyllama 10 hours ago
> Apple's .car file

> not related to Apple CarPlay

reply
adolph 9 hours ago
When you build an app with Xcode, your .xcassets folders are compiled into binary .car files that ship with your application. . . . The “CAR” extension likely stands for “Compiled Asset Record” based on method names found in Xcode’s IBFoundation framework.

The article is well worth reading even without ingesting the specifics in order to follow the author's reverse engineering process.

reply
promiseofbeans 15 hours ago
This is cool work. However, the author claims the following:

> This knowledge could be useful for security research and building developer tools that does not rely on Xcode or Apple’s proprietary tools.

Yes it could be. But if you developed it for such altruistic purposes, why tease the code?

> I’m considering open-sourcing these tools, but no promises yet!

Maybe OOP is thinking of selling their reverse engineering tools? Seems like that’s still a proprietary tool, I’m just paying someone else for it

reply
skrebbel 14 hours ago
I'm not sure it's about money. This maybe be increasingly hard to imagine in this age of AI-slop, but some devs actually don't want to publish code that is a terribly embarrassing mess, and prefer to clean it up first.
reply
worldsavior 7 hours ago
There is nothing bad with messy code unless you work with a team. Showing that you coded messy code doesn't make you a bad coder.
reply
kfkfkddnc 8 hours ago
Karu Karu please try
reply
zombot 12 hours ago
> _QWORD *__fastcall

Is that WinDOS shit?

Anyway, compiling to WASM is smart. Apple can't kill your tools if they're not on the app store. And you don't have to pay Apple tax for giving access to a free tool. Cool project!

reply
saagarjha 8 hours ago
It’s IDA being IDA
reply
silvestrov 15 hours ago
Looks very much like a format that should just have been gzipped JSON.

Don't use binary formats when it isn't absolutely needed.

reply
mike_hearn 14 hours ago
It's not new. BOMStore is a format inherited from NeXTStep. JSON didn't exist back then.

Also, it's a format designed to hold binary data. JSON can't do that without hacks like base64 encoding.

Binary file stores like this are very common in highly optimized software, which operating systems tend to be, especially if you go looking at the older parts. Windows has a similar format embedded in EXE/DLL files. Same concept: a kind of pseudo-filesystem used to hold app icons and other resources.

reply
MrBuddyCasino 10 hours ago
You don't have to put the files in the json, you can just put them inside the gzip file. But yeah obviously these are different eras.
reply
jasode 13 hours ago
>Looks very much like a format that should just have been gzipped JSON.

For application file formats that require storing binary blob data such as images, bitmaps, etc , many in the industry have settled on "SQLite db as a file format": (https://www.sqlite.org/appfileformat.html)

Examples include Mozilla Firefox using sqlite db for favicons, Apple iOS using sqlite to store camera photos, Kodi media player uses sqlite for binary metadata, Microsoft Visual C++ IDE stores source code browsing data in sqlite, etc.

Sqlite db would usually be a better choice rather than binary blobs encoded as Base64 and being stuffed into json.gzip files. One of the areas where the less efficient gzipped JSON might be better than Sqlite is web-server-to-web-browser data transfers because the client's Javascript engine has builtin gzip decompress and JSON manipulation functions.

reply
OskarS 10 hours ago
My favorite example of this is Audacity, which stores their entire project file, including all the audio data, as a SQLite database. It's really amazing, you can stream audio data from many input sources into a SQLite database at once, it wont break a sweat, it's flexible and extremely safe from data loss due to crashes or power outages. As well as trivially mutable: many of these kinds of formats the "export" is a heavy-duty operation which serializes everything to JSON or whatever. But in SQLite, it's just a question of updating the database with normal INSERTs, very cheap and simple operations. We've put a similar system into production, and it's incredible how well it works.
reply
trashb 13 hours ago
Every format is binary in the end, you are just swapping out the separators.

I personally subscribe to the Unix philosophy. There are really two options, binary or plain text (readable binary due to a agreed standard formatting). All other formats are a variation of the two.

Additional a binary format suits makes sense when the format is to be used for mobile devices which may not have much storage or bandwidth.

reply
lpcvoid 15 hours ago
Strong disagree. I like binary formats because I can just fopen(), fseek() and fread() stuff. I don't need json parser dependency, and don't need to deal with compression. Binary formats are simple, fast and I need a way smaller buffer to read/write them normally. I don't like wasting resources.
reply
high_na_euv 15 hours ago
Binary formats are painful to deal with from user perspective

Sane for 3rd party devs

reply
adolph 8 hours ago
Hazard a guess that serializing demented data structures into something text encoded like json or yaml or XML/SOAP is no less painful than a straight binary representation aside from unfamiliarity of tooling to reason about and arbitrarily query the structure, like jq, lq, etc.
reply
taneq 12 hours ago
Binary formats (that you can just fopen(), fseek(), fread() etc.) are generally super platform dependent. You can read in an array of bytes and then manually deserialise it (and I’ve done this, for sure!) but that’s basically one step away from parsing anyway.
reply
streetfighter64 10 hours ago
> Don't use binary formats when it isn't absolutely needed.

JSON in particular isn't very good [0] but I'd also argue that text formats in general aren't great. Isn't it a lot better that an int32_t always takes exactly 4 bytes instead of anywhere between one and eleven bytes (-2147483648)?

[0] https://news.ycombinator.com/item?id=40555431

reply
coderatlarge 7 hours ago
one use-case is if you need to be able to stream the data.
reply
peterspath 13 hours ago
I choose binary formats over JSON almost every time I can. JSON sucks big time.
reply
drob518 11 hours ago
JSON is… adequate. I like binary formats, too, when doing low-level programming, but you often want something readable, and JSON is a lot better and easier to parse than many other things, say XML.
reply
Cthulhu_ 11 hours ago
JSON is for humans to read and maintain, not machines to read/write - and machines read/write millions of times more than humans do.

Using JSON / HTTP for backend communication (i.e. microservices) is madness when you consider how much overhead there is and how little value having it be human-readable delivers.

reply
mort96 15 hours ago
Uh. You want to store assets in JSON? Why? You generally want asset packs to be seekable so that you can extract just one asset, and why would you want to add the overhead of parsing potentially gigabytes of JSON and then, per asset, decoding potentially tens of megabytes of base64?
reply
embedding-shape 15 hours ago
> You want to store assets in JSON? Why?

Why not have both options? .gltf and .glb being possible for assets been more than helpful to me more than once, having the option gives you the best of both worlds :)

reply
mort96 13 hours ago
What's Apple's incentive for having two different asset pack formats? It seems like more work to support parsing and generating both, and Apple expects you to use their tools to generate asset packs.

Working with binary files really isn't that hard. If Apple documented the .car format, writing your own parser wouldn't be difficult. It's not like it's a complicated format. Still, Apple clearly doesn't intend for people to make their own .car generators; to them, ease of reverse engineering is a bug, not a feature.

reply
pjmlp 11 hours ago
Only because they were originally designed for Web 3D.
reply
silvestrov 15 hours ago
Keep all the meta info in JSON and then the big binary files in a zip file. Much easier to parse.
reply
mort96 13 hours ago
Easier for the developer or easier for the computer?

Computers need to do it a bunch for every program launch for every single user of macOS for decades. The developer just needed to write a generator and a parser for the format once.

Would it have been a bit easier to write a parser for a format that's based around a zip file with a manifest.json? I don't know, maybe. You'd end up with some pretty huge dependencies (a zip file reader, a JSON parser), but maybe it'd be slightly easier. Is it worth it?

reply
ape4 15 hours ago
Someday JSON will be out of fashion - like XML is now.
reply
yrro 15 hours ago
... and that is why all 'modern' software is incredibly memory and CPU intensive...
reply
high_na_euv 15 hours ago
But when things go wrong, you can usually find some random json file and adjust it :)
reply
FrankBooth 13 hours ago
[flagged]
reply
empiricus 15 hours ago
Idea: pass the decompiled code through a "please rename variables according to their purpose" step using a coding agent. Not ideal, but arguably better than v03, v20. And almost zero effort at this time and age.
reply
lpcvoid 15 hours ago
And have it hallucinate stuff? Nah, this stuff is hard enough without LLMs guessing.
reply
empiricus 14 hours ago
Well, I mean just choosing better names, don't touch the actual code. and you can also add a basic human filtering step if you want. You cannot possible say that "v12" is better than "header.size". I would argue that even hallucinated names are good: you should be able to think "but this position variable is not quite correctly updated, maybe this is not the position", which seems better than "this v12 variable is updated in some complicated way which I will ignore because it has no meaning".
reply
streetfighter64 10 hours ago
In case the variable describes the number of input files, then v12 is better than header.size. How can you be sure that adding some LLM noise will provide actually accurate names?
reply
jitl 14 hours ago
i think for obj-c specifically (can’t speak to other langs) i’ve had a great experience. it does make little mistakes but ai oriented approach makes it faster/easier to find areas of interest to analyze or experiment with.

obj-c sendmsg use makes it more similar to understanding minified JS than decompiling static c because it literally calls many methods by string name.

reply
orbital-decay 12 hours ago
It's a labeling task with benign failure modes, much better suited for an LLM compared to generation
reply
sigseg1v 11 hours ago
If you ask an LLM to do a statically verifiable task without writing a simple verifier for it, and it hallucinates, that mistake is on you because it's a very quick step to guarantee something like this succeeds.
reply
streetfighter64 10 hours ago
I mean, step 0 is verifying that the code with changed names actually compiles. But step 1, which is way more difficult, is ensuring that replacing v01 with out_file_idx or whatever, actually gives a more accurate description of the purpose of v01. Otherwise, what's the point of generating names if they have a 10% chance of misleading more than clarifying.
reply
Cthulhu_ 11 hours ago
I'd argue that as long as it produced working code it's better than nothing, in this case.
reply
streetfighter64 10 hours ago
The code is already working with the v01, v02 names though. The use of LLMs here is intended to add information for humans to easier understand what the code does. Which might be worthwhile, but I think this AI upscaling of Obama pretty well illustrates the potential risks of trying to fill in information gaps without a proper understanding of the data https://x.com/Chicken3gg/status/1274314622447820801
reply