How to access AMF3 binary data from files (SOL)?

Hello!

I’m working on a project, converting an AIR desktop app to several new platforms using OpenFL, and I’m trying to figure out how to access the old app’s AMF3 binary data (which is stored in a local SharedObject file). This data contains licenses, personal settings and such, and I want to make sure the transition to the new desktop version for the users is as smooth as possible, so keeping previous settings would be great. (Note: I have to build native apps on desktop platforms, so AIR target from OpenFL is not possible)

Here’s what I’ve tried so far:

  • The appdata directory is different compared to the new OpenFL app even with the same ID. But I think it’s okay, because even if it’d be at the same location the SOL file format is totally different (AMF0 vs AMF3)
  • I managed to access the old app’s SOL files (it’s not nice, but working), and I can read the file into a ByteArray. However from this point I’m not able to read any AMF3 value from the bytearray, because the stored data is Object (in AS3) and ByteArray does not have a readObject() function in OpenFL

So now I’m stuck with how to read previously stored AMF3 data from files/bytearrays.

Anyone ever managed to successfully achieve this? Is there any other available method to access/process AMF3 data?

Thank you

This might work:

haxelib install format

then <haxelib name="format" /> in your project.xml

It looks like it would be format.amf3.Tools.decode (value); but it may require more testing

1 Like

Thank you.

It turned out data in SOL files is not even AMF3 but some sort of a unique binary. I ended up writing my own parser, took me the entire day :slight_smile:

Your comment was a big help, though, I didn’t know this sexy format lib existed :+1:

Yeah! It’s been a great help to some of the things we’ve needed to decode, for example, we use it for stuffing Flash assets into SWFs at compile time :wink:

I’m hearing another developer who is looking for something like this. Are you interested in sharing any of the work you’ve done?

Absolutely!

The code is currently only sufficient to read data from SOL of the software I’m working on, it’s not really mature enough to share and use for anything else, but I’m thinking of contributing to Haxe formats library once it’s fully done.

Although there are a few documentations available on AMF3, I couldn’t find anything useful for reading SOL files. And while the data is similar to AMF3, it’s not exactly the same, so it cannot be read with ByteArray.readObject() or with the aforementioned Haxe formats library (it gave inconsistent results on complex objects).

The docs I found are misleading in a few aspects, and the source code of FlashDevelop wasn’t helpful either, because while it has a perfect SOL reader (by ALESSANDRO CRUGNOLA), it’s only included as a DLL library. So I had to come up with my own parser.

Anyway here’s the idea for future reference, maybe someone will find it useful.

Note the method is not fail-safe, some aspects are totally unknown or only guessed, and there are probably better and easier techniques out there. Also it’s only for AMF3 object encoding!

The basic concept of parsing SOL byte data is this:

  • The first 17 bytes (header) can be completely ignored. An integer at byte index 2 stores the length of the data (file size minus header size), and there’s a “TCSO” string at byte index 6 which can be used to verify it really is a SharedObject file
  • The next data to parse is the name of the shared object starting with the length of the string which is stored on a single byte. So you have to read 1 byte as the length, then read length bytes as string (UTF8)
  • Then there’s an integer (4 bytes) which looks to be 3 in all of my test files. I believe it stores whether the data is AMF3 or AMF0 but I can’t say for sure. Who’s using AMF0 nowadays anyway :wink:
  • The next byte is the length of the property name. It’s a bit tricky, because it has to be shifted right bitwise (byteData >> 1) to get the real length. So a 0x0F byte data actually means that the name of the property is 0x07 bytes long
  • The next is to read length bytes as a string, which is the name of the property (as in so.data.myproperty)
  • Then comes the type of the data value (1 byte) called marker. Here’s a good read on the subject. Undefined, null, true and false are all stored only as markers, there’s no need to read anything further.
  • Then you have to read the actual value. This part is very complex because different types have to be read differently. I.e. Numbers are always 64 bit, Strings always start with the length and so on. More advanced types are a bit difficult to read. Object, ByteArray, Dictionary all have their unique aspects.
  • Each and every value ends with 0x0 byte. I believe it is a marker for “data end” in AMF.
  • There’s no footer whatsoever in the file, it just ends with the last 0x0 byte for the last property

I hope this all can be helpful to someone. I also recommend to create a basic Flash/AIR application and store all the possible types in a sharedobject to see how the SOL file looks. And yes, a good Hex editor/viewer can come in very handy

Maybe there’s an easier open-source SOL reader somewhere for parsing reference, I just didn’t have much time to look, and gave up searching for it.

1 Like

Thanks @igazine your info was really helpful. Just to clear something up, SharedObjects do use AMF3, however, they only encode the ‘value’ using AMF3, but knowing that and using the built-in tools removes the headache of special handling of each value type and having to parse manually for type markers.

I wrote a SharedObject parser today and thought I would share the code:

package;

import Reflect;
import haxe.io.BytesInput;
import openfl.utils.ByteArray;

class SharedObjectParser {

    private static var HEADER_LENGTH:Int                = 17;
    private static var HEADER_TYPE:String               = "TCSO";
    private static var HEADER_TYPE_POSITION:Int         = 6;
    private static var ENCODING:Int                     = 3; // amf3

    public function new() {}

    public static function parse(bytes:ByteArray):Dynamic
    {
        bytes.position = HEADER_TYPE_POSITION;
        var tag:String = bytes.readUTFBytes(4);
        
        if (tag != HEADER_TYPE)
        {
            trace("Invalid Shared Object File!");
        }

        var so:Dynamic = {};
        so.data = {};

        var bytesCopy:ByteArray = new ByteArray();
        bytesCopy.writeBytes(bytes, HEADER_LENGTH, bytes.length - HEADER_LENGTH);
        bytes = bytesCopy;
        bytes.position = 0;
        var soNameLength:Int = bytes.readByte();
        var soName:String = bytes.readUTFBytes(soNameLength);

        bytes.position += 3;
        var amf3:Bool = bytes.readByte() == ENCODING;

        if (!amf3)
        {
            trace("AMF3 Encoding Error!");
        }

        // start iterating over properties and values
        var propertyLength:Int;
        var propertyName:String;
        var bytesInput:BytesInput;
        var valueBytesCopy:ByteArray;
        var value:Dynamic;
        while (bytes.bytesAvailable > 0)
        {
            propertyLength = bytes.readByte() >> 1;
            propertyName = bytes.readUTFBytes(propertyLength);
            
            var emptyCount = 0;
            var startPos:Int = bytes.position;
            var eov:Bool = false; // end of value
            while (!eov)
            {
                if (bytes.readByte() == 0x0)
                {
                    if (bytes.bytesAvailable == 0)
                    {
                        eov = true;
                    }
                    else if (bytes.readByte() != 0x0)
                    {
                        bytes.position -= 1;
                        eov = true;
                    }
                }
            }

            valueBytesCopy = new ByteArray();
            valueBytesCopy.writeBytes(bytes, startPos, bytes.position - startPos);
            bytesInput = new BytesInput(valueBytesCopy);
            var reader = new format.amf3.Reader(bytesInput);
            var data = reader.read();
            value = format.amf3.Tools.decode(data);

            Reflect.setProperty(so.data, propertyName, value);
        }

        return so;
    }
}
2 Likes

Fantastic!

Thank you

We recently needed to migrate user’s old SharedObject AMF3 data to the Haxe-specific serialization format. The solution posted by @JRock24 didn’t work for us but we are posting an updated version that might be useful to someone else looking into this.

package;

import haxe.io.BytesInput;

import flash.utils.ByteArray;

class AMF3SharedObjectParser {

    private static var HEADER_LENGTH:Int                = 17;
    private static var HEADER_TYPE:String               = "TCSO";
    private static var HEADER_TYPE_POSITION:Int         = 6;
    private static var ENCODING:Int                     = 3; // amf3

    public function new() {}

    public static function parse(bytes:ByteArray):Dynamic
    {
        var bytesInput:BytesInput = new BytesInput(bytes);
        
        /// Get the SharedObject header type and abort if incorrect.
        bytesInput.position = HEADER_TYPE_POSITION;
        var soTag:String = bytesInput.readString(4);
        if (soTag != HEADER_TYPE)
        {
            trace("Invalid Shared Object File!");
            return null;
        }

        /// Get the SharedObject full name from the header.
        bytesInput.position = HEADER_LENGTH;
        var soNameLength:Int = bytesInput.readInt8();
        var soName:String = bytesInput.readString(soNameLength);

        /// Get the SharedObject encoding and abort if not correct.
        bytesInput.position += 3;
        var amf3:Bool = bytesInput.readInt8() == ENCODING;
        if (!amf3)
        {
            trace("AMF3 Encoding Error!");
            return null;
        }

        var data:Dynamic = {};
        var name:String;
        var value:Dynamic;
        var reader = new format.amf3.Reader(bytesInput);

        /// Start iterating over properties and values.
        while (bytesInput.position < bytesInput.length)
        {
            name = format.amf3.Tools.decode(reader.readWithCode(0x06));
            value = format.amf3.Tools.decode(reader.read());

            if(Std.is(value, format.amf3.Amf3Array))
                value = value.a;

            Reflect.setProperty(data, name, value);

            bytesInput.position += 1;
        }

        return { data: data };
    }
2 Likes