Ibtool replacement

There are a few packages which seem to require ibtool. This binary comes from the full Xcode rather than cli tools for some reason, and that would make the package non-free.

In a moment of madness I thought - what’s to stop me from reimplementing it? Turns out there’s a project on GH already which can process some iOS xib files, but it’s extremely alpha and has bugs. But after a few changes, I can process the smallest of Transmission’s xib files. (With lots of attributes still missing, but still)

But before I dig too deep and polish the results to be 100% matching:

  1. Has anyone tried to do that yet? Are there any known “this will not work because…” reasons?
  2. Has anyone thought about it and collected any minimal .xib files? Just getting enough good samples is an annoying job.

Pinging some Darwin people just in case :wink: @reckenrode @emily

3 Likes

I tried it once for the Vulkan-Tools cube demo, but it didn’t work. That may be one simple example. Another could be the Mac pinentry for GNUPG. There are translated nibs checked into nixpkgs, which might be useful as references for intended output. They’re not minimal, but the apps are not complicated.

Edit: Added pinentry links. The cube demo is using storyboards, which may not be what you wantz

I don’t think there’s any fundamental reason it couldn’t be done, just… nobody has wanted to do it :slight_smile: If you got somewhere with it, that would be incredible!

Although I warn you that your next job would be to write a Metal shader compiler that targets an undocumented IR… (and xcbuild really needs a maintainer or a rewrite…)

your next job would be to write a Metal shader compiler

I’m up for some fun RE, not for a hard labor punishment :stuck_out_tongue:

Just in case someone looks for this in the future, search keywords: imhex nib pattern reverse engineering

import std.mem;
import std.io;
import std.core;
import std.string;

using FlexNumber;

fn flex_value(ref FlexNumber num) {
    u32 res = 0;
    for (u8 i=0, i < sizeof(num.values), i=i+1){
        res = res + ((num.values[i] & 0x7f) << (i*7));
    }
    return res;
};

struct FlexNumber {
    u32 _location = $;
    u8 values[while($ == _location || std::mem::read_unsigned($-1, 1) & 0x80 == 0)];
} [[format_read("flex_value"),transform("flex_value")]];

struct Class {
    FlexNumber length;
    u8 tp;
    if(tp == 0x81) {
        u32 unknown;
    }
    char name[length];
};

fn class_mapping(ref FlexNumber idx) {
    return classes[idx].name;
};

struct ObjectEntry {
    FlexNumber class_idx [[format_read("class_mapping")]];
    FlexNumber props_first_idx;
    FlexNumber num_props;
} [[single_color]];

struct Label {
    FlexNumber length [[hidden]];
    char value[length];
} [[single_color,inline]];

enum Encoding : u8 {
    Byte = 0,
    Short = 1,
    Word = 2,
    DWord = 3,
    False = 4,
    True = 5,
    Float = 6,
    Double = 7,
    String = 8,
    Nil = 9,
    Object = 10,
};

fn label_mapping(ref FlexNumber idx) {
    return labels[idx].value;
};

fn object_ref(u32 id) {
    return std::format("@{}", id);
};

using Property;

fn prop_string(ref Property prop) {
    str label = labels[prop.label_idx].value;
    str type = std::string::substr(std::core::formatted_value(prop.encoding), 10, 20);
    if(std::core::has_member(prop, "value")) {
        return std::format("{}/{} ({})", label, prop.value, type);
    } else {
        return std::format("{}/{}", label, type);
    }
};

struct Property {
    FlexNumber label_idx [[format_read("label_mapping")]];
    Encoding encoding;
    match(encoding) {
        (Encoding::Byte): u8 value;
        (Encoding::Short): u16 value;
        (Encoding::Word): u32 value;
        (Encoding::DWord): u64 value;
        (Encoding::Float): float value;
        (Encoding::Double): double value;
        (Encoding::String): {
            FlexNumber length;
            char value[length];
        }
        (Encoding::Object): u32 value [[format_read("object_ref")]];
    }
} [[single_color,format_read("prop_string")]];

struct Section {
    u32 obj_count;
    u32 address;
};

struct Header {
    u32 size;
    u32 section_count = (size-1)/2;
    Section sections[section_count];
};

char magic[10]@0;
u32 unknown@10;
Header header@14;

Section cls_section = header.sections[3];
Class classes[cls_section.obj_count] @ cls_section.address;

Section objs_section = header.sections[0];
ObjectEntry objs[objs_section.obj_count] @ objs_section.address;

Section labels_section = header.sections[1];
Label labels[labels_section.obj_count] @ labels_section.address;

Section props_section = header.sections[2];
Property props[props_section.obj_count] @ props_section.address;

The project is at GitHub - viraptor/ibtool: iOS Interface Builder utility, implemented in python.
I had to break it a bit to remove bad assumptions, but the minimal sample is 1:1 compatible with Apple’s output. I’ll keep adding small bits to it until Transmission can be compiled this way.

1 Like

Just as a followup, I’ve got a few simple/test xibs compiling correctly and one big one from Transmission. A couple more weeks and this may be actually usable.

2 Likes

Where I’m currently stuck and would love for someone experienced in Xibs to explain something to me is:

Why does (abridged):

        <window>
            <rect key="contentRect" x="641" y="589" width="538" height="337"/>
            <clipView key="contentView" id="123">
                <rect key="frame" x="1" y="1" width="523" height="188"/>
                <subviews>
                    <textView id="28">
                        <rect key="frame" x="0.0" y="0.0" width="523" height="188"/>

Result in NSFrameSize = {538, 337} rather than {538, 188} on the textView.
You can test this on current master on ibtool with:

./test.sh samples/test7_clipview.xib

or see the whole compiled version with

python ibtool.py --dump -s -e samples/test7_clipview.nib

Or maybe I just need to stare into the Nib abyss some more…

Two more weeks were not enough. But I’ve got a few more files from transmission passing almost 100%. The remaining work is:

  • mostly just copy-pasting values for new control types
  • the small-but-annoying unresolved difference is frame size calculation which is off by a few px

The biggest time sink was discovering that some details and behaviours depend on the toolsVersion attribute in the Xib files. I’ll have to find the actual thresholds one day rather than depending on checking for >= value from test files I have. Apple’s ibtool preserves bug-for-bug compatibility going many versions back.

Anyway, building transmission should be possible soon™

2 Likes

So excited for this!

As I get closer to some release (4 files from transmission succeed, all others are mostly ok), I’m starting to wonder what’s the good model for long-term maintenance.
The big issue with an open replacement is that there’s no good way to tell when an edge case is being hit. Even with lots of asserts and exceptions on unknown values, some areas are still basically magic. I’m trying to keep a reasonable set of manually recreated test cases so I don’t run into a “files from other project” licence issues, but that doesn’t stop new edge cases and new IB versions from appearing. Which means we’re virtually guaranteed to get a package update one day which looks funky but just via nixpkgs.

The only idea I’ve got at the moment is to have a repo with one submodule for each currently supported project and run a weekly summary, comparing my tool’s output to Apple’s for each xib found. This is likely going to be a pain :joy: but it may be the only way to catch issues before they land in actual future releases. And not keeping them in the same repo works around licensing.

Other ideas welcome. Of course it’s going to be easier if the project actually gets more people involved, but it’s such a weird thing I’m not holding my breath.

1 Like

The project page for getting transmission to work is at ibtool transmission · GitHub

I thought this project can avoid “the other 90%” problem, but then discovered today that even basic xibs used in transmission can compile to recursive nibs. And I blame Apple…