RAR (Roshall ARchiver) archive files: Java parsing library

RAR is a archive format used by popular proprietary RAR archiver, created by Eugene Roshal. There are two major versions of format (v1.5-4.0 and RAR v5+).

File format essentially consists of a linear sequence of blocks. Each block has fixed header and custom body (that depends on block type), so it's possible to skip block even if one doesn't know how to process a certain block type.

Application

RAR archiver

File extension

rar

KS implementation details

Minimal Kaitai Struct required: 0.7

This page hosts a formal specification of RAR (Roshall ARchiver) archive files using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Usage

Parse a local file and get structure in memory:

Rar data = Rar.fromFile("path/to/local/file.rar");

Or parse structure from a byte array:

byte[] someArray = new byte[] { ... };
Rar data = new Rar(new KaitaiStream(someArray));

After that, one can get various attributes from the structure by invoking getter methods like:

data.magic() // => File format signature to validate that it is indeed a RAR archive

Java source code to parse RAR (Roshall ARchiver) archive files

Rar.java

// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

import io.kaitai.struct.ByteBufferKaitaiStream;
import io.kaitai.struct.KaitaiStruct;
import io.kaitai.struct.KaitaiStream;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;


/**
 * RAR is a archive format used by popular proprietary RAR archiver,
 * created by Eugene Roshal. There are two major versions of format
 * (v1.5-4.0 and RAR v5+).
 * 
 * File format essentially consists of a linear sequence of
 * blocks. Each block has fixed header and custom body (that depends on
 * block type), so it's possible to skip block even if one doesn't know
 * how to process a certain block type.
 */
public class Rar extends KaitaiStruct {
    public static Rar fromFile(String fileName) throws IOException {
        return new Rar(new ByteBufferKaitaiStream(fileName));
    }

    public enum BlockTypes {
        MARKER(114),
        ARCHIVE_HEADER(115),
        FILE_HEADER(116),
        OLD_STYLE_COMMENT_HEADER(117),
        OLD_STYLE_AUTHENTICITY_INFO_76(118),
        OLD_STYLE_SUBBLOCK(119),
        OLD_STYLE_RECOVERY_RECORD(120),
        OLD_STYLE_AUTHENTICITY_INFO_79(121),
        SUBBLOCK(122),
        TERMINATOR(123);

        private final long id;
        BlockTypes(long id) { this.id = id; }
        public long id() { return id; }
        private static final Map<Long, BlockTypes> byId = new HashMap<Long, BlockTypes>(10);
        static {
            for (BlockTypes e : BlockTypes.values())
                byId.put(e.id(), e);
        }
        public static BlockTypes byId(long id) { return byId.get(id); }
    }

    public enum Oses {
        MS_DOS(0),
        OS_2(1),
        WINDOWS(2),
        UNIX(3),
        MAC_OS(4),
        BEOS(5);

        private final long id;
        Oses(long id) { this.id = id; }
        public long id() { return id; }
        private static final Map<Long, Oses> byId = new HashMap<Long, Oses>(6);
        static {
            for (Oses e : Oses.values())
                byId.put(e.id(), e);
        }
        public static Oses byId(long id) { return byId.get(id); }
    }

    public enum Methods {
        STORE(48),
        FASTEST(49),
        FAST(50),
        NORMAL(51),
        GOOD(52),
        BEST(53);

        private final long id;
        Methods(long id) { this.id = id; }
        public long id() { return id; }
        private static final Map<Long, Methods> byId = new HashMap<Long, Methods>(6);
        static {
            for (Methods e : Methods.values())
                byId.put(e.id(), e);
        }
        public static Methods byId(long id) { return byId.get(id); }
    }

    public Rar(KaitaiStream _io) {
        this(_io, null, null);
    }

    public Rar(KaitaiStream _io, KaitaiStruct _parent) {
        this(_io, _parent, null);
    }

    public Rar(KaitaiStream _io, KaitaiStruct _parent, Rar _root) {
        super(_io);
        this._parent = _parent;
        this._root = _root == null ? this : _root;
        _read();
    }
    private void _read() {
        this.magic = new MagicSignature(this._io, this, _root);
        this.blocks = new ArrayList<KaitaiStruct>();
        {
            int i = 0;
            while (!this._io.isEof()) {
                switch (magic().version()) {
                case 0: {
                    this.blocks.add(new Block(this._io, this, _root));
                    break;
                }
                case 1: {
                    this.blocks.add(new BlockV5(this._io, this, _root));
                    break;
                }
                }
                i++;
            }
        }
    }
    public static class BlockV5 extends KaitaiStruct {
        public static BlockV5 fromFile(String fileName) throws IOException {
            return new BlockV5(new ByteBufferKaitaiStream(fileName));
        }

        public BlockV5(KaitaiStream _io) {
            this(_io, null, null);
        }

        public BlockV5(KaitaiStream _io, Rar _parent) {
            this(_io, _parent, null);
        }

        public BlockV5(KaitaiStream _io, Rar _parent, Rar _root) {
            super(_io);
            this._parent = _parent;
            this._root = _root;
            _read();
        }
        private void _read() {
        }
        private Rar _root;
        private Rar _parent;
        public Rar _root() { return _root; }
        public Rar _parent() { return _parent; }
    }

    /**
     * Basic block that RAR files consist of. There are several block
     * types (see `block_type`), which have different `body` and
     * `add_body`.
     */
    public static class Block extends KaitaiStruct {
        public static Block fromFile(String fileName) throws IOException {
            return new Block(new ByteBufferKaitaiStream(fileName));
        }

        public Block(KaitaiStream _io) {
            this(_io, null, null);
        }

        public Block(KaitaiStream _io, Rar _parent) {
            this(_io, _parent, null);
        }

        public Block(KaitaiStream _io, Rar _parent, Rar _root) {
            super(_io);
            this._parent = _parent;
            this._root = _root;
            _read();
        }
        private void _read() {
            this.crc16 = this._io.readU2le();
            this.blockType = Rar.BlockTypes.byId(this._io.readU1());
            this.flags = this._io.readU2le();
            this.blockSize = this._io.readU2le();
            if (hasAdd()) {
                this.addSize = this._io.readU4le();
            }
            switch (blockType()) {
            case FILE_HEADER: {
                this._raw_body = this._io.readBytes(bodySize());
                KaitaiStream _io__raw_body = new ByteBufferKaitaiStream(_raw_body);
                this.body = new BlockFileHeader(_io__raw_body, this, _root);
                break;
            }
            default: {
                this.body = this._io.readBytes(bodySize());
                break;
            }
            }
            if (hasAdd()) {
                this.addBody = this._io.readBytes(addSize());
            }
        }
        private Boolean hasAdd;

        /**
         * True if block has additional content attached to it
         */
        public Boolean hasAdd() {
            if (this.hasAdd != null)
                return this.hasAdd;
            boolean _tmp = (boolean) ((flags() & 32768) != 0);
            this.hasAdd = _tmp;
            return this.hasAdd;
        }
        private Byte headerSize;
        public Byte headerSize() {
            if (this.headerSize != null)
                return this.headerSize;
            byte _tmp = (byte) ((hasAdd() ? 11 : 7));
            this.headerSize = _tmp;
            return this.headerSize;
        }
        private Integer bodySize;
        public Integer bodySize() {
            if (this.bodySize != null)
                return this.bodySize;
            int _tmp = (int) ((blockSize() - headerSize()));
            this.bodySize = _tmp;
            return this.bodySize;
        }
        private int crc16;
        private BlockTypes blockType;
        private int flags;
        private int blockSize;
        private Long addSize;
        private Object body;
        private byte[] addBody;
        private Rar _root;
        private Rar _parent;
        private byte[] _raw_body;

        /**
         * CRC16 of whole block or some part of it (depends on block type)
         */
        public int crc16() { return crc16; }
        public BlockTypes blockType() { return blockType; }
        public int flags() { return flags; }

        /**
         * Size of block (header + body, but without additional content)
         */
        public int blockSize() { return blockSize; }

        /**
         * Size of additional content in this block
         */
        public Long addSize() { return addSize; }
        public Object body() { return body; }

        /**
         * Additional content in this block
         */
        public byte[] addBody() { return addBody; }
        public Rar _root() { return _root; }
        public Rar _parent() { return _parent; }
        public byte[] _raw_body() { return _raw_body; }
    }
    public static class BlockFileHeader extends KaitaiStruct {
        public static BlockFileHeader fromFile(String fileName) throws IOException {
            return new BlockFileHeader(new ByteBufferKaitaiStream(fileName));
        }

        public BlockFileHeader(KaitaiStream _io) {
            this(_io, null, null);
        }

        public BlockFileHeader(KaitaiStream _io, Rar.Block _parent) {
            this(_io, _parent, null);
        }

        public BlockFileHeader(KaitaiStream _io, Rar.Block _parent, Rar _root) {
            super(_io);
            this._parent = _parent;
            this._root = _root;
            _read();
        }
        private void _read() {
            this.lowUnpSize = this._io.readU4le();
            this.hostOs = Rar.Oses.byId(this._io.readU1());
            this.fileCrc32 = this._io.readU4le();
            this.fileTime = new DosTime(this._io, this, _root);
            this.rarVersion = this._io.readU1();
            this.method = Rar.Methods.byId(this._io.readU1());
            this.nameSize = this._io.readU2le();
            this.attr = this._io.readU4le();
            if ((_parent().flags() & 256) != 0) {
                this.highPackSize = this._io.readU4le();
            }
            this.fileName = this._io.readBytes(nameSize());
            if ((_parent().flags() & 1024) != 0) {
                this.salt = this._io.readU8le();
            }
        }
        private long lowUnpSize;
        private Oses hostOs;
        private long fileCrc32;
        private DosTime fileTime;
        private int rarVersion;
        private Methods method;
        private int nameSize;
        private long attr;
        private Long highPackSize;
        private byte[] fileName;
        private Long salt;
        private Rar _root;
        private Rar.Block _parent;

        /**
         * Uncompressed file size (lower 32 bits, if 64-bit header flag is present)
         */
        public long lowUnpSize() { return lowUnpSize; }

        /**
         * Operating system used for archiving
         */
        public Oses hostOs() { return hostOs; }
        public long fileCrc32() { return fileCrc32; }

        /**
         * Date and time in standard MS DOS format
         */
        public DosTime fileTime() { return fileTime; }

        /**
         * RAR version needed to extract file (Version number is encoded as 10 * Major version + minor version.)
         */
        public int rarVersion() { return rarVersion; }

        /**
         * Compression method
         */
        public Methods method() { return method; }

        /**
         * File name size
         */
        public int nameSize() { return nameSize; }

        /**
         * File attributes
         */
        public long attr() { return attr; }

        /**
         * Compressed file size, high 32 bits, only if 64-bit header flag is present
         */
        public Long highPackSize() { return highPackSize; }
        public byte[] fileName() { return fileName; }
        public Long salt() { return salt; }
        public Rar _root() { return _root; }
        public Rar.Block _parent() { return _parent; }
    }

    /**
     * RAR uses either 7-byte magic for RAR versions 1.5 to 4.0, and
     * 8-byte magic (and pretty different block format) for v5+. This
     * type would parse and validate both versions of signature. Note
     * that actually this signature is a valid RAR "block": in theory,
     * one can omit signature reading at all, and read this normally,
     * as a block, if exact RAR version is known (and thus it's
     * possible to choose correct block format).
     */
    public static class MagicSignature extends KaitaiStruct {
        public static MagicSignature fromFile(String fileName) throws IOException {
            return new MagicSignature(new ByteBufferKaitaiStream(fileName));
        }

        public MagicSignature(KaitaiStream _io) {
            this(_io, null, null);
        }

        public MagicSignature(KaitaiStream _io, Rar _parent) {
            this(_io, _parent, null);
        }

        public MagicSignature(KaitaiStream _io, Rar _parent, Rar _root) {
            super(_io);
            this._parent = _parent;
            this._root = _root;
            _read();
        }
        private void _read() {
            this.magic1 = this._io.ensureFixedContents(new byte[] { 82, 97, 114, 33, 26, 7 });
            this.version = this._io.readU1();
            if (version() == 1) {
                this.magic3 = this._io.ensureFixedContents(new byte[] { 0 });
            }
        }
        private byte[] magic1;
        private int version;
        private byte[] magic3;
        private Rar _root;
        private Rar _parent;

        /**
         * Fixed part of file's magic signature that doesn't change with RAR version
         */
        public byte[] magic1() { return magic1; }

        /**
         * Variable part of magic signature: 0 means old (RAR 1.5-4.0)
         * format, 1 means new (RAR 5+) format
         */
        public int version() { return version; }

        /**
         * New format (RAR 5+) magic contains extra byte
         */
        public byte[] magic3() { return magic3; }
        public Rar _root() { return _root; }
        public Rar _parent() { return _parent; }
    }
    public static class DosTime extends KaitaiStruct {
        public static DosTime fromFile(String fileName) throws IOException {
            return new DosTime(new ByteBufferKaitaiStream(fileName));
        }

        public DosTime(KaitaiStream _io) {
            this(_io, null, null);
        }

        public DosTime(KaitaiStream _io, Rar.BlockFileHeader _parent) {
            this(_io, _parent, null);
        }

        public DosTime(KaitaiStream _io, Rar.BlockFileHeader _parent, Rar _root) {
            super(_io);
            this._parent = _parent;
            this._root = _root;
            _read();
        }
        private void _read() {
            this.time = this._io.readU2le();
            this.date = this._io.readU2le();
        }
        private Integer month;
        public Integer month() {
            if (this.month != null)
                return this.month;
            int _tmp = (int) (((date() & 480) >> 5));
            this.month = _tmp;
            return this.month;
        }
        private Integer seconds;
        public Integer seconds() {
            if (this.seconds != null)
                return this.seconds;
            int _tmp = (int) (((time() & 31) * 2));
            this.seconds = _tmp;
            return this.seconds;
        }
        private Integer year;
        public Integer year() {
            if (this.year != null)
                return this.year;
            int _tmp = (int) ((((date() & 65024) >> 9) + 1980));
            this.year = _tmp;
            return this.year;
        }
        private Integer minutes;
        public Integer minutes() {
            if (this.minutes != null)
                return this.minutes;
            int _tmp = (int) (((time() & 2016) >> 5));
            this.minutes = _tmp;
            return this.minutes;
        }
        private Integer day;
        public Integer day() {
            if (this.day != null)
                return this.day;
            int _tmp = (int) ((date() & 31));
            this.day = _tmp;
            return this.day;
        }
        private Integer hours;
        public Integer hours() {
            if (this.hours != null)
                return this.hours;
            int _tmp = (int) (((time() & 63488) >> 11));
            this.hours = _tmp;
            return this.hours;
        }
        private int time;
        private int date;
        private Rar _root;
        private Rar.BlockFileHeader _parent;
        public int time() { return time; }
        public int date() { return date; }
        public Rar _root() { return _root; }
        public Rar.BlockFileHeader _parent() { return _parent; }
    }
    private MagicSignature magic;
    private ArrayList<KaitaiStruct> blocks;
    private Rar _root;
    private KaitaiStruct _parent;

    /**
     * File format signature to validate that it is indeed a RAR archive
     */
    public MagicSignature magic() { return magic; }

    /**
     * Sequence of blocks that constitute the RAR file
     */
    public ArrayList<KaitaiStruct> blocks() { return blocks; }
    public Rar _root() { return _root; }
    public KaitaiStruct _parent() { return _parent; }
}