MiniFS is a file system found in certain firmware files from TP-Link
(including its Mercusys brand) and Mercury. Such firmware contains an RTOS
kernel, e.g. VxWorks in
RE450(V4)_210531.zip
(archived)
or TPOS, TP-Link's RTOS, in
TL-WA850RE(EU)_V7.0_250808.zip
(archived).
According to a TP-Link manager quoted in a
2021 article
(archived),
Mercusys has been sold in China as Mercury since 2001. All Mercury images
examined contain a certificate with O=tplink in its issuer and subject.
All versions share the same header, and its version field determines the
layout of the rest of the image.
In version 3, the header is followed by a table of directory paths and file names, followed by meta information for each file, meta information about the LZMA compressed blobs including length, offset and uncompressed length, and then a number of LZMA compressed blobs.
The LZMA blobs can contain the concatenated contents of multiple files. The meta information of a file contains the offsets of its directory path and file name, the LZMA blob (numbering starts at 0), the offset of the file in the uncompressed data and the size of the uncompressed file.
Some version 3 images have no uncompressed length in the meta information
about the blobs (see has_8_byte_lzma_metas in body_v3).
In version 2, the header is followed by the length of the files area and a meta information record for each file, each followed directly by the file's data. Unlike versions 1 and 3, version 2 stores no paths or file names at all, only the MD5 hash of each file's path.
In version 1, the header is followed by a record with the path, the compressed length and the offset of each file, and then one LZMA blob per file.
This Kaitai Struct implementation was written independently of the paper Reverse Engineered MiniFS File System. It was started in the firmware analysis project BANG (Binary Analysis Next Generation) in 2021 and submitted to the Kaitai Struct format gallery in September 2023, whereas the paper was published in July 2024.
This page hosts a formal specification of TP-Link MiniFS using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.
// 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.Objects;
import io.kaitai.struct.ConsistencyError;
import java.util.ArrayList;
import java.util.List;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* MiniFS is a file system found in certain firmware files from TP-Link
* (including its Mercusys brand) and Mercury. Such firmware contains an RTOS
* kernel, e.g. VxWorks in
* [`RE450(V4)_210531.zip`](https://static.tp-link.com/firmware/2021/202107/20210709/RE450(V4)_210531.zip)
* ([archived](https://web.archive.org/web/20261105175157/https://static.tp-link.com/firmware/2021/202107/20210709/RE450(V4)_210531.zip))
* or TPOS, TP-Link's RTOS, in
* [`TL-WA850RE(EU)_V7.0_250808.zip`](https://static.tp-link.com/upload/firmware/2025/202509/20250919/TL-WA850RE(EU)_V7.0_250808.zip)
* ([archived](https://web.archive.org/web/20260903180047/https://static.tp-link.com/upload/firmware/2025/202509/20250919/TL-WA850RE(EU)_V7.0_250808.zip)).
*
* According to a TP-Link manager quoted in a
* [2021 article](https://shiftdelete.net/tp-link-mercusys-ile-ilgili-hedeflerini-paylasti)
* ([archived](https://web.archive.org/web/20230513175833/https://shiftdelete.net/tp-link-mercusys-ile-ilgili-hedeflerini-paylasti)),
* Mercusys has been sold in China as Mercury since 2001. All Mercury images
* examined contain a certificate with `O=tplink` in its issuer and subject.
*
* All versions share the same header, and its `version` field determines the
* layout of the rest of the image.
*
* In version 3, the header is followed by a table of directory paths and file
* names, followed by meta information for each file, meta information about
* the LZMA compressed blobs including length, offset and uncompressed length,
* and then a number of LZMA compressed blobs.
*
* The LZMA blobs can contain the concatenated contents of multiple files.
* The meta information of a file contains the offsets of its directory path
* and file name, the LZMA blob (numbering starts at 0), the offset of the file
* in the uncompressed data and the size of the uncompressed file.
*
* Some version 3 images have no uncompressed length in the meta information
* about the blobs (see `has_8_byte_lzma_metas` in `body_v3`).
*
* In version 2, the header is followed by the length of the files area and a
* meta information record for each file, each followed directly by the file's
* data. Unlike versions 1 and 3, version 2 stores no paths or file names at
* all, only the MD5 hash of each file's path.
*
* In version 1, the header is followed by a record with the path, the compressed
* length and the offset of each file, and then one LZMA blob per file.
*
* This Kaitai Struct implementation was written independently of the paper
* [Reverse Engineered MiniFS File System](https://arxiv.org/abs/2407.05064v1).
* It was started in the firmware analysis project
* [BANG](https://github.com/armijnhemel/binaryanalysis-ng) (Binary Analysis
* Next Generation) in
* [2021](https://github.com/armijnhemel/binaryanalysis-ng/commit/531203a076e3761116320b77b2b9c64a9dca6186)
* and submitted to the Kaitai Struct format gallery in
* [September 2023](https://github.com/kaitai-io/kaitai_struct_formats/pull/682),
* whereas the paper was published in July 2024.
* @see <a href="https://arxiv.org/html/2407.05064v1#S4">The paper, section 4 (version 3)</a>
* @see <a href="https://github.com/zh-explorer/tp_research/blob/a42f9d0427ad0c453a7060f3236c4174bb883441/Minifs.py">tp_research's `Minifs.py` (version 2)</a>
* @see <a href="https://github.com/onekey-sec/unblob/blob/678dbce13dcabe3b4ff16b2b958d6f2cfce9ae3f/python/unblob/handlers/filesystem/minifs.py">unblob's MiniFS handler (version 3)</a>
*/
public class TplinkMinifs extends KaitaiStruct.ReadWrite {
public static TplinkMinifs fromFile(String fileName) throws IOException {
return new TplinkMinifs(new ByteBufferKaitaiStream(fileName));
}
public enum CompressionType {
LZMA(0),
RAW(2),
LZMA_2014(4);
private final long id;
CompressionType(long id) { this.id = id; }
public long id() { return id; }
private static final Map<Long, CompressionType> byId = new HashMap<Long, CompressionType>(3);
static {
for (CompressionType e : CompressionType.values())
byId.put(e.id(), e);
}
public static CompressionType byId(long id) { return byId.get(id); }
}
public TplinkMinifs() {
this(null, null, null);
}
public TplinkMinifs(KaitaiStream _io) {
this(_io, null, null);
}
public TplinkMinifs(KaitaiStream _io, KaitaiStruct.ReadWrite _parent) {
this(_io, _parent, null);
}
public TplinkMinifs(KaitaiStream _io, KaitaiStruct.ReadWrite _parent, TplinkMinifs _root) {
super(_io);
this._parent = _parent;
this._root = _root == null ? this : _root;
}
public void _read() {
this.header = new MinifsHeader(this._io, this, _root);
this.header._read();
switch (header().version()) {
case 1: {
this.body = new BodyV1(this._io, this, _root);
((BodyV1) (this.body))._read();
break;
}
case 2: {
this.body = new BodyV2(this._io, this, _root);
((BodyV2) (this.body))._read();
break;
}
case 3: {
this.body = new BodyV3(this._io, this, _root);
((BodyV3) (this.body))._read();
break;
}
}
_dirty = false;
}
public void _fetchInstances() {
this.header._fetchInstances();
switch (header().version()) {
case 1: {
((BodyV1) (this.body))._fetchInstances();
break;
}
case 2: {
((BodyV2) (this.body))._fetchInstances();
break;
}
case 3: {
((BodyV3) (this.body))._fetchInstances();
break;
}
}
}
public void _write_Seq() {
_assertNotDirty();
this.header._write_Seq(this._io);
switch (header().version()) {
case 1: {
((BodyV1) (this.body))._write_Seq(this._io);
break;
}
case 2: {
((BodyV2) (this.body))._write_Seq(this._io);
break;
}
case 3: {
((BodyV3) (this.body))._write_Seq(this._io);
break;
}
}
}
public void _check() {
if (!Objects.equals(this.header._root(), _root()))
throw new ConsistencyError("header", _root(), this.header._root());
if (!Objects.equals(this.header._parent(), this))
throw new ConsistencyError("header", this, this.header._parent());
switch (header().version()) {
case 1: {
if (!Objects.equals(((TplinkMinifs.BodyV1) (this.body))._root(), _root()))
throw new ConsistencyError("body", _root(), ((TplinkMinifs.BodyV1) (this.body))._root());
if (!Objects.equals(((TplinkMinifs.BodyV1) (this.body))._parent(), this))
throw new ConsistencyError("body", this, ((TplinkMinifs.BodyV1) (this.body))._parent());
break;
}
case 2: {
if (!Objects.equals(((TplinkMinifs.BodyV2) (this.body))._root(), _root()))
throw new ConsistencyError("body", _root(), ((TplinkMinifs.BodyV2) (this.body))._root());
if (!Objects.equals(((TplinkMinifs.BodyV2) (this.body))._parent(), this))
throw new ConsistencyError("body", this, ((TplinkMinifs.BodyV2) (this.body))._parent());
break;
}
case 3: {
if (!Objects.equals(((TplinkMinifs.BodyV3) (this.body))._root(), _root()))
throw new ConsistencyError("body", _root(), ((TplinkMinifs.BodyV3) (this.body))._root());
if (!Objects.equals(((TplinkMinifs.BodyV3) (this.body))._parent(), this))
throw new ConsistencyError("body", this, ((TplinkMinifs.BodyV3) (this.body))._parent());
break;
}
}
_dirty = false;
}
/**
* @see <a href="https://arxiv.org/html/2407.05064v1#S2.SS2">Source</a>
*/
public static class BodyV1 extends KaitaiStruct.ReadWrite {
public static BodyV1 fromFile(String fileName) throws IOException {
return new BodyV1(new ByteBufferKaitaiStream(fileName));
}
public BodyV1() {
this(null, null, null);
}
public BodyV1(KaitaiStream _io) {
this(_io, null, null);
}
public BodyV1(KaitaiStream _io, TplinkMinifs _parent) {
this(_io, _parent, null);
}
public BodyV1(KaitaiStream _io, TplinkMinifs _parent, TplinkMinifs _root) {
super(_io);
this._parent = _parent;
this._root = _root;
}
public void _read() {
this.inodes = new ArrayList<InodeV1>();
for (int i = 0; i < _root().header().numFiles(); i++) {
InodeV1 _t_inodes = new InodeV1(this._io, this, _root, ((Number) ((i != 0 ? inodes().get(((Number) (i - 1)).intValue()).ofsEnd() : 0))).longValue());
try {
_t_inodes._read();
} finally {
this.inodes.add(_t_inodes);
}
}
this._raw_blobsArea = this._io.readBytes(inodes().get(inodes().size() - 1).ofsEnd());
KaitaiStream _io__raw_blobsArea = new ByteBufferKaitaiStream(this._raw_blobsArea);
this.blobsArea = new Dummy(_io__raw_blobsArea, this, _root);
this.blobsArea._read();
_dirty = false;
}
public void _fetchInstances() {
for (int i = 0; i < this.inodes.size(); i++) {
this.inodes.get(((Number) (i)).intValue())._fetchInstances();
}
this.blobsArea._fetchInstances();
}
public void _write_Seq() {
_assertNotDirty();
for (int i = 0; i < this.inodes.size(); i++) {
this.inodes.get(((Number) (i)).intValue())._write_Seq(this._io);
}
final KaitaiStream _io__raw_blobsArea = new ByteBufferKaitaiStream(inodes().get(inodes().size() - 1).ofsEnd());
this._io.addChildStream(_io__raw_blobsArea);
{
long _pos2 = this._io.pos();
this._io.seek(this._io.pos() + (inodes().get(inodes().size() - 1).ofsEnd()));
final BodyV1 _this = this;
_io__raw_blobsArea.setWriteBackHandler(new KaitaiStream.WriteBackHandler(_pos2) {
@Override
protected void write(KaitaiStream parent) {
_this._raw_blobsArea = _io__raw_blobsArea.toByteArray();
if (_this._raw_blobsArea.length != inodes().get(inodes().size() - 1).ofsEnd())
throw new ConsistencyError("raw(blobs_area)", inodes().get(inodes().size() - 1).ofsEnd(), _this._raw_blobsArea.length);
parent.writeBytes(_this._raw_blobsArea);
}
});
}
this.blobsArea._write_Seq(_io__raw_blobsArea);
}
public void _check() {
if (this.inodes.size() != _root().header().numFiles())
throw new ConsistencyError("inodes", _root().header().numFiles(), this.inodes.size());
for (int i = 0; i < this.inodes.size(); i++) {
if (!Objects.equals(this.inodes.get(((Number) (i)).intValue())._root(), _root()))
throw new ConsistencyError("inodes", _root(), this.inodes.get(((Number) (i)).intValue())._root());
if (!Objects.equals(this.inodes.get(((Number) (i)).intValue())._parent(), this))
throw new ConsistencyError("inodes", this, this.inodes.get(((Number) (i)).intValue())._parent());
if (this.inodes.get(((Number) (i)).intValue()).prevOfsEnd() != ((Number) ((i != 0 ? inodes().get(((Number) (i - 1)).intValue()).ofsEnd() : 0))).longValue())
throw new ConsistencyError("inodes", ((Number) ((i != 0 ? inodes().get(((Number) (i - 1)).intValue()).ofsEnd() : 0))).longValue(), this.inodes.get(((Number) (i)).intValue()).prevOfsEnd());
}
if (!Objects.equals(this.blobsArea._root(), _root()))
throw new ConsistencyError("blobs_area", _root(), this.blobsArea._root());
if (!Objects.equals(this.blobsArea._parent(), this))
throw new ConsistencyError("blobs_area", this, this.blobsArea._parent());
_dirty = false;
}
private List<InodeV1> inodes;
private Dummy blobsArea;
private TplinkMinifs _root;
private TplinkMinifs _parent;
private byte[] _raw_blobsArea;
public List<InodeV1> inodes() { return inodes; }
public void setInodes(List<InodeV1> _v) { _dirty = true; inodes = _v; }
public Dummy blobsArea() { return blobsArea; }
public void setBlobsArea(Dummy _v) { _dirty = true; blobsArea = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs _parent() { return _parent; }
public void set_parent(TplinkMinifs _v) { _dirty = true; _parent = _v; }
public byte[] _raw_blobsArea() { return _raw_blobsArea; }
public void set_raw_BlobsArea(byte[] _v) { _dirty = true; _raw_blobsArea = _v; }
}
public static class BodyV2 extends KaitaiStruct.ReadWrite {
public static BodyV2 fromFile(String fileName) throws IOException {
return new BodyV2(new ByteBufferKaitaiStream(fileName));
}
public BodyV2() {
this(null, null, null);
}
public BodyV2(KaitaiStream _io) {
this(_io, null, null);
}
public BodyV2(KaitaiStream _io, TplinkMinifs _parent) {
this(_io, _parent, null);
}
public BodyV2(KaitaiStream _io, TplinkMinifs _parent, TplinkMinifs _root) {
super(_io);
this._parent = _parent;
this._root = _root;
}
public void _read() {
this.lenFilesArea = this._io.readU4be();
this._raw_filesArea = this._io.readBytes(lenFilesArea());
KaitaiStream _io__raw_filesArea = new ByteBufferKaitaiStream(this._raw_filesArea);
this.filesArea = new FilesAreaV2(_io__raw_filesArea, this, _root);
this.filesArea._read();
_dirty = false;
}
public void _fetchInstances() {
this.filesArea._fetchInstances();
}
public void _write_Seq() {
_assertNotDirty();
this._io.writeU4be(this.lenFilesArea);
final KaitaiStream _io__raw_filesArea = new ByteBufferKaitaiStream(lenFilesArea());
this._io.addChildStream(_io__raw_filesArea);
{
long _pos2 = this._io.pos();
this._io.seek(this._io.pos() + (lenFilesArea()));
final BodyV2 _this = this;
_io__raw_filesArea.setWriteBackHandler(new KaitaiStream.WriteBackHandler(_pos2) {
@Override
protected void write(KaitaiStream parent) {
_this._raw_filesArea = _io__raw_filesArea.toByteArray();
if (_this._raw_filesArea.length != lenFilesArea())
throw new ConsistencyError("raw(files_area)", lenFilesArea(), _this._raw_filesArea.length);
parent.writeBytes(_this._raw_filesArea);
}
});
}
this.filesArea._write_Seq(_io__raw_filesArea);
}
public void _check() {
if (!Objects.equals(this.filesArea._root(), _root()))
throw new ConsistencyError("files_area", _root(), this.filesArea._root());
if (!Objects.equals(this.filesArea._parent(), this))
throw new ConsistencyError("files_area", this, this.filesArea._parent());
_dirty = false;
}
private long lenFilesArea;
private FilesAreaV2 filesArea;
private TplinkMinifs _root;
private TplinkMinifs _parent;
private byte[] _raw_filesArea;
public long lenFilesArea() { return lenFilesArea; }
public void setLenFilesArea(long _v) { _dirty = true; lenFilesArea = _v; }
public FilesAreaV2 filesArea() { return filesArea; }
public void setFilesArea(FilesAreaV2 _v) { _dirty = true; filesArea = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs _parent() { return _parent; }
public void set_parent(TplinkMinifs _v) { _dirty = true; _parent = _v; }
public byte[] _raw_filesArea() { return _raw_filesArea; }
public void set_raw_FilesArea(byte[] _v) { _dirty = true; _raw_filesArea = _v; }
}
public static class BodyV3 extends KaitaiStruct.ReadWrite {
public static BodyV3 fromFile(String fileName) throws IOException {
return new BodyV3(new ByteBufferKaitaiStream(fileName));
}
public BodyV3() {
this(null, null, null);
}
public BodyV3(KaitaiStream _io) {
this(_io, null, null);
}
public BodyV3(KaitaiStream _io, TplinkMinifs _parent) {
this(_io, _parent, null);
}
public BodyV3(KaitaiStream _io, TplinkMinifs _parent, TplinkMinifs _root) {
super(_io);
this._parent = _parent;
this._root = _root;
}
public void _read() {
this.lenNameTable = this._io.readU4be();
this._raw_nameTable = this._io.readBytes(lenNameTable());
KaitaiStream _io__raw_nameTable = new ByteBufferKaitaiStream(this._raw_nameTable);
this.nameTable = new NameTable(_io__raw_nameTable, this, _root);
this.nameTable._read();
this.inodes = new ArrayList<Inode>();
for (int i = 0; i < _root().header().numFiles(); i++) {
Inode _t_inodes = new Inode(this._io, this, _root, ((Number) ((i != 0 ? inodes().get(((Number) (i - 1)).intValue()).curMaxLzmaBlob() : 0))).longValue());
try {
_t_inodes._read();
} finally {
this.inodes.add(_t_inodes);
}
}
if (ofsLzmaMetas() < 0) {
this._unnamed3 = this._io.readBytes(0);
}
this.lzmaMetas = new ArrayList<LzmaMeta>();
for (int i = 0; i < numLzmaBlobs(); i++) {
LzmaMeta _t_lzmaMetas = new LzmaMeta(this._io, this, _root, ((Number) ((i != 0 ? lzmaMetas().get(((Number) (i - 1)).intValue()).ofsEnd() : 0))).longValue());
try {
_t_lzmaMetas._read();
} finally {
this.lzmaMetas.add(_t_lzmaMetas);
}
}
this._raw_lzmaBlobsArea = this._io.readBytes(lzmaMetas().get(lzmaMetas().size() - 1).ofsEnd());
KaitaiStream _io__raw_lzmaBlobsArea = new ByteBufferKaitaiStream(this._raw_lzmaBlobsArea);
this.lzmaBlobsArea = new Dummy(_io__raw_lzmaBlobsArea, this, _root);
this.lzmaBlobsArea._read();
_dirty = false;
}
public void _fetchInstances() {
this.nameTable._fetchInstances();
for (int i = 0; i < this.inodes.size(); i++) {
this.inodes.get(((Number) (i)).intValue())._fetchInstances();
}
if (ofsLzmaMetas() < 0) {
}
for (int i = 0; i < this.lzmaMetas.size(); i++) {
this.lzmaMetas.get(((Number) (i)).intValue())._fetchInstances();
}
this.lzmaBlobsArea._fetchInstances();
lenFirstBlobUncompressedIf8ByteMetas();
if (this.lenFirstBlobUncompressedIf8ByteMetas != null) {
}
}
public void _write_Seq() {
_assertNotDirty();
_shouldWriteLenFirstBlobUncompressedIf8ByteMetas = _enabledLenFirstBlobUncompressedIf8ByteMetas;
this._io.writeU4be(this.lenNameTable);
final KaitaiStream _io__raw_nameTable = new ByteBufferKaitaiStream(lenNameTable());
this._io.addChildStream(_io__raw_nameTable);
{
long _pos2 = this._io.pos();
this._io.seek(this._io.pos() + (lenNameTable()));
final BodyV3 _this = this;
_io__raw_nameTable.setWriteBackHandler(new KaitaiStream.WriteBackHandler(_pos2) {
@Override
protected void write(KaitaiStream parent) {
_this._raw_nameTable = _io__raw_nameTable.toByteArray();
if (_this._raw_nameTable.length != lenNameTable())
throw new ConsistencyError("raw(name_table)", lenNameTable(), _this._raw_nameTable.length);
parent.writeBytes(_this._raw_nameTable);
}
});
}
this.nameTable._write_Seq(_io__raw_nameTable);
for (int i = 0; i < this.inodes.size(); i++) {
this.inodes.get(((Number) (i)).intValue())._write_Seq(this._io);
}
if (ofsLzmaMetas() < 0) {
if (this._unnamed3.length != 0)
throw new ConsistencyError("_unnamed3", 0, this._unnamed3.length);
this._io.writeBytes(this._unnamed3);
}
for (int i = 0; i < this.lzmaMetas.size(); i++) {
this.lzmaMetas.get(((Number) (i)).intValue())._write_Seq(this._io);
}
final KaitaiStream _io__raw_lzmaBlobsArea = new ByteBufferKaitaiStream(lzmaMetas().get(lzmaMetas().size() - 1).ofsEnd());
this._io.addChildStream(_io__raw_lzmaBlobsArea);
{
long _pos2 = this._io.pos();
this._io.seek(this._io.pos() + (lzmaMetas().get(lzmaMetas().size() - 1).ofsEnd()));
final BodyV3 _this = this;
_io__raw_lzmaBlobsArea.setWriteBackHandler(new KaitaiStream.WriteBackHandler(_pos2) {
@Override
protected void write(KaitaiStream parent) {
_this._raw_lzmaBlobsArea = _io__raw_lzmaBlobsArea.toByteArray();
if (_this._raw_lzmaBlobsArea.length != lzmaMetas().get(lzmaMetas().size() - 1).ofsEnd())
throw new ConsistencyError("raw(lzma_blobs_area)", lzmaMetas().get(lzmaMetas().size() - 1).ofsEnd(), _this._raw_lzmaBlobsArea.length);
parent.writeBytes(_this._raw_lzmaBlobsArea);
}
});
}
this.lzmaBlobsArea._write_Seq(_io__raw_lzmaBlobsArea);
}
public void _check() {
if (!Objects.equals(this.nameTable._root(), _root()))
throw new ConsistencyError("name_table", _root(), this.nameTable._root());
if (!Objects.equals(this.nameTable._parent(), this))
throw new ConsistencyError("name_table", this, this.nameTable._parent());
if (this.inodes.size() != _root().header().numFiles())
throw new ConsistencyError("inodes", _root().header().numFiles(), this.inodes.size());
for (int i = 0; i < this.inodes.size(); i++) {
if (!Objects.equals(this.inodes.get(((Number) (i)).intValue())._root(), _root()))
throw new ConsistencyError("inodes", _root(), this.inodes.get(((Number) (i)).intValue())._root());
if (!Objects.equals(this.inodes.get(((Number) (i)).intValue())._parent(), this))
throw new ConsistencyError("inodes", this, this.inodes.get(((Number) (i)).intValue())._parent());
if (this.inodes.get(((Number) (i)).intValue()).prevMaxLzmaBlob() != ((Number) ((i != 0 ? inodes().get(((Number) (i - 1)).intValue()).curMaxLzmaBlob() : 0))).longValue())
throw new ConsistencyError("inodes", ((Number) ((i != 0 ? inodes().get(((Number) (i - 1)).intValue()).curMaxLzmaBlob() : 0))).longValue(), this.inodes.get(((Number) (i)).intValue()).prevMaxLzmaBlob());
}
if (this.lzmaMetas.size() != numLzmaBlobs())
throw new ConsistencyError("lzma_metas", numLzmaBlobs(), this.lzmaMetas.size());
for (int i = 0; i < this.lzmaMetas.size(); i++) {
if (!Objects.equals(this.lzmaMetas.get(((Number) (i)).intValue())._root(), _root()))
throw new ConsistencyError("lzma_metas", _root(), this.lzmaMetas.get(((Number) (i)).intValue())._root());
if (!Objects.equals(this.lzmaMetas.get(((Number) (i)).intValue())._parent(), this))
throw new ConsistencyError("lzma_metas", this, this.lzmaMetas.get(((Number) (i)).intValue())._parent());
if (this.lzmaMetas.get(((Number) (i)).intValue()).prevOfsEnd() != ((Number) ((i != 0 ? lzmaMetas().get(((Number) (i - 1)).intValue()).ofsEnd() : 0))).longValue())
throw new ConsistencyError("lzma_metas", ((Number) ((i != 0 ? lzmaMetas().get(((Number) (i - 1)).intValue()).ofsEnd() : 0))).longValue(), this.lzmaMetas.get(((Number) (i)).intValue()).prevOfsEnd());
}
if (!Objects.equals(this.lzmaBlobsArea._root(), _root()))
throw new ConsistencyError("lzma_blobs_area", _root(), this.lzmaBlobsArea._root());
if (!Objects.equals(this.lzmaBlobsArea._parent(), this))
throw new ConsistencyError("lzma_blobs_area", this, this.lzmaBlobsArea._parent());
if (_enabledLenFirstBlobUncompressedIf8ByteMetas) {
}
_dirty = false;
}
private Boolean has8ByteLzmaMetas;
/**
* Whether each record in `lzma_metas` lacks `len_blob_uncompressed` and
* is therefore 8 bytes in size instead of 12.
*
* The MiniFS header doesn't say which size is used (both use version 3),
* so this Kaitai Struct implementation infers it using a heuristic based
* on the `.lzma` header of the first blob. Bytes 5 to 12 of that header
* are the uncompressed size, which was unknown (all bits set) for all
* blobs in all version 3 images examined. So with 8-byte records,
* `len_first_blob_uncompressed_if_8_byte_metas` has all bits set. With
* 12-byte records, the same 8 bytes aren't the uncompressed size. With 3
* or more blobs, they include a whole offset or length in `lzma_metas`,
* which would have to be 4 GiB. With 1 or 2 blobs, they include the
* first blob's dictionary size, which is 8 MiB in all images examined.
*
* An image with 8-byte records and a known uncompressed size would be
* rejected, or misparsed if it has a single blob and other data follows it.
*
* In the images examined, 8-byte records occur in firmware files with
* modification times from 2018 to 2022 (as stored in their ZIPs), e.g. in
* [`MW301R(EU)_V2_180821.zip`](https://static.mercusys.com/software/MW301R(EU)_V2_180821.zip)
* ([archived](https://web.archive.org/web/20261105093950/https://static.mercusys.com/software/MW301R(EU)_V2_180821.zip)),
* listed on <https://www.mercusys.com/en/download/mw301r/v2/>
* ([archived](https://web.archive.org/web/20261105142659/https://www.mercusys.com/en/download/mw301r/v2/)).
*
* As far as we know, this implementation is the first description of
* 8-byte records. The other descriptions and tools found either assume
* 12 bytes or don't read `lzma_metas` at all. So there is no reference
* to compare it with, and the evidence comes from the images. In all 49
* distinct images examined with 8-byte records, the bytes at each
* record's offset and length are exactly one complete LZMA stream. Each
* such stream decompresses to exactly the length that the files packed
* into it need. The file that ends last in it ends exactly where the
* decompressed data ends. In addition:
*
* * 2614 of the 7230 files in these 49 images have the same path and
* contents as a file in an image with 12-byte records. For example,
* 49 files are the same in the image with 8-byte records in
* [`TL-WA850RE(EU)_V7_201029.zip`](https://static.tp-link.com/2021/202102/20210209/TL-WA850RE(EU)_V7_201029.zip)
* ([archived](https://web.archive.org/web/20220326032207/https://static.tp-link.com/2021/202102/20210209/TL-WA850RE(EU)_V7_201029.zip))
* and the image with 12-byte records in
* [`TL-WA850RE(EU)_V7.0_250808.zip`](https://static.tp-link.com/upload/firmware/2025/202509/20250919/TL-WA850RE(EU)_V7.0_250808.zip)
* ([archived](https://web.archive.org/web/20260903180047/https://static.tp-link.com/upload/firmware/2025/202509/20250919/TL-WA850RE(EU)_V7.0_250808.zip)).
* * The repository
* [NurMohammadDuari/mw325r-hack-lab](https://github.com/NurMohammadDuari/mw325r-hack-lab/tree/024da680dbf4f9ca287d562622f02d7d8875bd45)
* has the [files](https://github.com/NurMohammadDuari/mw325r-hack-lab/tree/024da680dbf4f9ca287d562622f02d7d8875bd45/extracted-minifs)
* of the image with 8-byte records in
* [`MW325R(EU)_V2_20111620201211065426.zip`](https://static.mercusys.com/software/MW325R(EU)_V2_20111620201211065426.zip)
* ([archived](https://web.archive.org/web/20261105093427/https://static.mercusys.com/software/MW325R(EU)_V2_20111620201211065426.zip)).
* They match the files parsed by this implementation in all 92 files
* that can be compared. Its
* [extractor](https://github.com/NurMohammadDuari/mw325r-hack-lab/blob/024da680dbf4f9ca287d562622f02d7d8875bd45/analysis/minifs_extract.py)
* finds the blobs by searching for LZMA headers instead of reading
* `lzma_metas`.
*/
public Boolean has8ByteLzmaMetas() {
if (this.has8ByteLzmaMetas != null)
return this.has8ByteLzmaMetas;
this.has8ByteLzmaMetas = lenFirstBlobUncompressedIf8ByteMetas() == 0xffffffffffffffffL;
return this.has8ByteLzmaMetas;
}
public void _invalidateHas8ByteLzmaMetas() { this.has8ByteLzmaMetas = null; }
private Long lenFirstBlobUncompressedIf8ByteMetas;
private boolean _shouldWriteLenFirstBlobUncompressedIf8ByteMetas = false;
private boolean _enabledLenFirstBlobUncompressedIf8ByteMetas = true;
/**
* The uncompressed size in the `.lzma` header of the first blob, if
* `lzma_metas` have 8-byte records. It follows the properties byte and the
* 4-byte dictionary size.
*/
public Long lenFirstBlobUncompressedIf8ByteMetas() {
if (_shouldWriteLenFirstBlobUncompressedIf8ByteMetas)
_writeLenFirstBlobUncompressedIf8ByteMetas();
if (this.lenFirstBlobUncompressedIf8ByteMetas != null)
return this.lenFirstBlobUncompressedIf8ByteMetas;
if (!_enabledLenFirstBlobUncompressedIf8ByteMetas)
return null;
long _pos = this._io.pos();
this._io.seek((ofsLzmaMetas() + 8 * numLzmaBlobs()) + 5);
this.lenFirstBlobUncompressedIf8ByteMetas = this._io.readU8le();
this._io.seek(_pos);
return this.lenFirstBlobUncompressedIf8ByteMetas;
}
public void setLenFirstBlobUncompressedIf8ByteMetas(long _v) { _dirty = true; lenFirstBlobUncompressedIf8ByteMetas = _v; }
public void setLenFirstBlobUncompressedIf8ByteMetas_Enabled(boolean _v) { _dirty = true; _enabledLenFirstBlobUncompressedIf8ByteMetas = _v; }
private void _writeLenFirstBlobUncompressedIf8ByteMetas() {
_shouldWriteLenFirstBlobUncompressedIf8ByteMetas = false;
long _pos = this._io.pos();
this._io.seek((ofsLzmaMetas() + 8 * numLzmaBlobs()) + 5);
this._io.writeU8le(this.lenFirstBlobUncompressedIf8ByteMetas);
this._io.seek(_pos);
}
private Byte lenLzmaMeta;
/**
* The size of each record in `lzma_metas` (8 or 12 bytes).
*/
public Byte lenLzmaMeta() {
if (this.lenLzmaMeta != null)
return this.lenLzmaMeta;
this.lenLzmaMeta = ((Number) ((has8ByteLzmaMetas() ? 8 : 12))).byteValue();
return this.lenLzmaMeta;
}
public void _invalidateLenLzmaMeta() { this.lenLzmaMeta = null; }
private Integer numLzmaBlobs;
/**
* Count starts at 0, so there is one more blob than the highest blob number
* @see <a href="https://github.com/onekey-sec/unblob/blob/678dbce13dcabe3b4ff16b2b958d6f2cfce9ae3f/python/unblob/handlers/filesystem/minifs.py#L129-L130">Source</a>
*/
public Integer numLzmaBlobs() {
if (this.numLzmaBlobs != null)
return this.numLzmaBlobs;
this.numLzmaBlobs = ((Number) (inodes().get(inodes().size() - 1).curMaxLzmaBlob() + 1)).intValue();
return this.numLzmaBlobs;
}
public void _invalidateNumLzmaBlobs() { this.numLzmaBlobs = null; }
private Integer ofsLzmaMetas;
public Integer ofsLzmaMetas() {
if (this.ofsLzmaMetas != null)
return this.ofsLzmaMetas;
this.ofsLzmaMetas = ((Number) (_io().pos())).intValue();
return this.ofsLzmaMetas;
}
public void _invalidateOfsLzmaMetas() { this.ofsLzmaMetas = null; }
private long lenNameTable;
private NameTable nameTable;
private List<Inode> inodes;
private byte[] _unnamed3;
private List<LzmaMeta> lzmaMetas;
private Dummy lzmaBlobsArea;
private TplinkMinifs _root;
private TplinkMinifs _parent;
private byte[] _raw_nameTable;
private byte[] _raw_lzmaBlobsArea;
public long lenNameTable() { return lenNameTable; }
public void setLenNameTable(long _v) { _dirty = true; lenNameTable = _v; }
public NameTable nameTable() { return nameTable; }
public void setNameTable(NameTable _v) { _dirty = true; nameTable = _v; }
public List<Inode> inodes() { return inodes; }
public void setInodes(List<Inode> _v) { _dirty = true; inodes = _v; }
public byte[] _unnamed3() { return _unnamed3; }
public void set_unnamed3(byte[] _v) { _dirty = true; _unnamed3 = _v; }
public List<LzmaMeta> lzmaMetas() { return lzmaMetas; }
public void setLzmaMetas(List<LzmaMeta> _v) { _dirty = true; lzmaMetas = _v; }
public Dummy lzmaBlobsArea() { return lzmaBlobsArea; }
public void setLzmaBlobsArea(Dummy _v) { _dirty = true; lzmaBlobsArea = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs _parent() { return _parent; }
public void set_parent(TplinkMinifs _v) { _dirty = true; _parent = _v; }
public byte[] _raw_nameTable() { return _raw_nameTable; }
public void set_raw_NameTable(byte[] _v) { _dirty = true; _raw_nameTable = _v; }
public byte[] _raw_lzmaBlobsArea() { return _raw_lzmaBlobsArea; }
public void set_raw_LzmaBlobsArea(byte[] _v) { _dirty = true; _raw_lzmaBlobsArea = _v; }
}
public static class Dummy extends KaitaiStruct.ReadWrite {
public static Dummy fromFile(String fileName) throws IOException {
return new Dummy(new ByteBufferKaitaiStream(fileName));
}
public Dummy() {
this(null, null, null);
}
public Dummy(KaitaiStream _io) {
this(_io, null, null);
}
public Dummy(KaitaiStream _io, KaitaiStruct.ReadWrite _parent) {
this(_io, _parent, null);
}
public Dummy(KaitaiStream _io, KaitaiStruct.ReadWrite _parent, TplinkMinifs _root) {
super(_io);
this._parent = _parent;
this._root = _root;
}
public void _read() {
_dirty = false;
}
public void _fetchInstances() {
}
public void _write_Seq() {
_assertNotDirty();
}
public void _check() {
_dirty = false;
}
private TplinkMinifs _root;
private KaitaiStruct.ReadWrite _parent;
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public KaitaiStruct.ReadWrite _parent() { return _parent; }
public void set_parent(KaitaiStruct.ReadWrite _v) { _dirty = true; _parent = _v; }
}
/**
* @see <a href="https://github.com/zh-explorer/tp_research/blob/a42f9d0427ad0c453a7060f3236c4174bb883441/Minifs.py#L41-L52">Source</a>
*/
public static class FileV2 extends KaitaiStruct.ReadWrite {
public static FileV2 fromFile(String fileName) throws IOException {
return new FileV2(new ByteBufferKaitaiStream(fileName));
}
public FileV2() {
this(null, null, null);
}
public FileV2(KaitaiStream _io) {
this(_io, null, null);
}
public FileV2(KaitaiStream _io, TplinkMinifs.FilesAreaV2 _parent) {
this(_io, _parent, null);
}
public FileV2(KaitaiStream _io, TplinkMinifs.FilesAreaV2 _parent, TplinkMinifs _root) {
super(_io);
this._parent = _parent;
this._root = _root;
}
public void _read() {
this.pathMd5 = this._io.readBytes(16);
this.lenData = this._io.readU4be();
this.ofsData = this._io.readU4be();
if (!(this.ofsData == _io().pos() + 8)) {
throw new KaitaiStream.ValidationNotEqualError(_io().pos() + 8, this.ofsData, this._io, "/types/file_v2/seq/2");
}
this.lenDataUncompressed = this._io.readU4be();
this.compression = TplinkMinifs.CompressionType.byId(this._io.readU4be());
this.data = this._io.readBytes(lenData());
_dirty = false;
}
public void _fetchInstances() {
}
public void _write_Seq() {
_assertNotDirty();
this._io.writeBytes(this.pathMd5);
this._io.writeU4be(this.lenData);
this._io.writeU4be(this.ofsData);
this._io.writeU4be(this.lenDataUncompressed);
this._io.writeU4be(((Number) (this.compression.id())).longValue());
this._io.writeBytes(this.data);
}
public void _check() {
if (this.pathMd5.length != 16)
throw new ConsistencyError("path_md5", 16, this.pathMd5.length);
if (this.data.length != lenData())
throw new ConsistencyError("data", lenData(), this.data.length);
_dirty = false;
}
private byte[] pathMd5;
private long lenData;
private long ofsData;
private long lenDataUncompressed;
private CompressionType compression;
private byte[] data;
private TplinkMinifs _root;
private TplinkMinifs.FilesAreaV2 _parent;
/**
* The MD5 hash of the absolute path of the file, e.g. of
* `/conf/priv-key.pem`.
*
* Version 2 images store only these hashes, not the paths, so a path can
* only be found by hashing candidate paths and comparing.
* [`Minifs.py`](https://github.com/zh-explorer/tp_research/blob/a42f9d0427ad0c453a7060f3236c4174bb883441/Minifs.py#L181-L209)
* takes the candidates from path-like strings in the files' contents.
*/
public byte[] pathMd5() { return pathMd5; }
public void setPathMd5(byte[] _v) { _dirty = true; pathMd5 = _v; }
public long lenData() { return lenData; }
public void setLenData(long _v) { _dirty = true; lenData = _v; }
/**
* Offset of `data` relative to the start of `<body_v2>.files_area`.
*/
public long ofsData() { return ofsData; }
public void setOfsData(long _v) { _dirty = true; ofsData = _v; }
public long lenDataUncompressed() { return lenDataUncompressed; }
public void setLenDataUncompressed(long _v) { _dirty = true; lenDataUncompressed = _v; }
public CompressionType compression() { return compression; }
public void setCompression(CompressionType _v) { _dirty = true; compression = _v; }
/**
* With `compression_type::lzma`, an LZMA stream in the `.lzma` format
* (also known as LZMA_Alone). Decompress it in the same way as
* `<lzma_meta>.blob`.
*
* In all images examined, the `.lzma` header specifies the properties
* lc = 0, lp = 0, pb = 2 and an 8 MiB dictionary, so it starts with the
* bytes `5A 00 00 80 00`. It also specifies the uncompressed size of the
* stream, and there is no end-of-payload marker.
*/
public byte[] data() { return data; }
public void setData(byte[] _v) { _dirty = true; data = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs.FilesAreaV2 _parent() { return _parent; }
public void set_parent(TplinkMinifs.FilesAreaV2 _v) { _dirty = true; _parent = _v; }
}
public static class FilesAreaV2 extends KaitaiStruct.ReadWrite {
public static FilesAreaV2 fromFile(String fileName) throws IOException {
return new FilesAreaV2(new ByteBufferKaitaiStream(fileName));
}
public FilesAreaV2() {
this(null, null, null);
}
public FilesAreaV2(KaitaiStream _io) {
this(_io, null, null);
}
public FilesAreaV2(KaitaiStream _io, TplinkMinifs.BodyV2 _parent) {
this(_io, _parent, null);
}
public FilesAreaV2(KaitaiStream _io, TplinkMinifs.BodyV2 _parent, TplinkMinifs _root) {
super(_io);
this._parent = _parent;
this._root = _root;
}
public void _read() {
this.files = new ArrayList<FileV2>();
for (int i = 0; i < _root().header().numFiles(); i++) {
FileV2 _t_files = new FileV2(this._io, this, _root);
try {
_t_files._read();
} finally {
this.files.add(_t_files);
}
}
_dirty = false;
}
public void _fetchInstances() {
for (int i = 0; i < this.files.size(); i++) {
this.files.get(((Number) (i)).intValue())._fetchInstances();
}
}
public void _write_Seq() {
_assertNotDirty();
for (int i = 0; i < this.files.size(); i++) {
this.files.get(((Number) (i)).intValue())._write_Seq(this._io);
}
}
public void _check() {
if (this.files.size() != _root().header().numFiles())
throw new ConsistencyError("files", _root().header().numFiles(), this.files.size());
for (int i = 0; i < this.files.size(); i++) {
if (!Objects.equals(this.files.get(((Number) (i)).intValue())._root(), _root()))
throw new ConsistencyError("files", _root(), this.files.get(((Number) (i)).intValue())._root());
if (!Objects.equals(this.files.get(((Number) (i)).intValue())._parent(), this))
throw new ConsistencyError("files", this, this.files.get(((Number) (i)).intValue())._parent());
}
_dirty = false;
}
private List<FileV2> files;
private TplinkMinifs _root;
private TplinkMinifs.BodyV2 _parent;
public List<FileV2> files() { return files; }
public void setFiles(List<FileV2> _v) { _dirty = true; files = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs.BodyV2 _parent() { return _parent; }
public void set_parent(TplinkMinifs.BodyV2 _v) { _dirty = true; _parent = _v; }
}
/**
* @see <a href="https://arxiv.org/html/2407.05064v1#S4.SS3">Source</a>
* @see <a href="https://github.com/onekey-sec/unblob/blob/678dbce13dcabe3b4ff16b2b958d6f2cfce9ae3f/python/unblob/handlers/filesystem/minifs.py#L36-L42">Source</a>
*/
public static class Inode extends KaitaiStruct.ReadWrite {
public Inode(long prevMaxLzmaBlob) {
this(null, null, null, prevMaxLzmaBlob);
}
public Inode(KaitaiStream _io, long prevMaxLzmaBlob) {
this(_io, null, null, prevMaxLzmaBlob);
}
public Inode(KaitaiStream _io, TplinkMinifs.BodyV3 _parent, long prevMaxLzmaBlob) {
this(_io, _parent, null, prevMaxLzmaBlob);
}
public Inode(KaitaiStream _io, TplinkMinifs.BodyV3 _parent, TplinkMinifs _root, long prevMaxLzmaBlob) {
super(_io);
this._parent = _parent;
this._root = _root;
this.prevMaxLzmaBlob = prevMaxLzmaBlob;
}
public void _read() {
this.ofsDirectoryPath = this._io.readU4be();
this.ofsFileName = this._io.readU4be();
this.lzmaBlob = this._io.readU4be();
this.ofsFile = this._io.readU4be();
this.lenFile = this._io.readU4be();
_dirty = false;
}
public void _fetchInstances() {
directoryPath();
if (this.directoryPath != null) {
}
fileName();
if (this.fileName != null) {
}
}
public void _write_Seq() {
_assertNotDirty();
_shouldWriteDirectoryPath = _enabledDirectoryPath;
_shouldWriteFileName = _enabledFileName;
this._io.writeU4be(this.ofsDirectoryPath);
this._io.writeU4be(this.ofsFileName);
this._io.writeU4be(this.lzmaBlob);
this._io.writeU4be(this.ofsFile);
this._io.writeU4be(this.lenFile);
}
public void _check() {
if (_enabledDirectoryPath) {
if (KaitaiStream.byteArrayIndexOf((this.directoryPath).getBytes(Charset.forName("ASCII")), ((byte) 0)) != -1)
throw new ConsistencyError("directory_path", -1, KaitaiStream.byteArrayIndexOf((this.directoryPath).getBytes(Charset.forName("ASCII")), ((byte) 0)));
}
if (_enabledFileName) {
if (KaitaiStream.byteArrayIndexOf((this.fileName).getBytes(Charset.forName("ASCII")), ((byte) 0)) != -1)
throw new ConsistencyError("file_name", -1, KaitaiStream.byteArrayIndexOf((this.fileName).getBytes(Charset.forName("ASCII")), ((byte) 0)));
}
_dirty = false;
}
private Integer curMaxLzmaBlob;
public Integer curMaxLzmaBlob() {
if (this.curMaxLzmaBlob != null)
return this.curMaxLzmaBlob;
this.curMaxLzmaBlob = ((Number) ((lzmaBlob() > prevMaxLzmaBlob() ? lzmaBlob() : prevMaxLzmaBlob()))).intValue();
return this.curMaxLzmaBlob;
}
public void _invalidateCurMaxLzmaBlob() { this.curMaxLzmaBlob = null; }
private String directoryPath;
private boolean _shouldWriteDirectoryPath = false;
private boolean _enabledDirectoryPath = true;
public String directoryPath() {
if (_shouldWriteDirectoryPath)
_writeDirectoryPath();
if (this.directoryPath != null)
return this.directoryPath;
if (!_enabledDirectoryPath)
return null;
KaitaiStream io = _parent().nameTable()._io();
long _pos = io.pos();
io.seek(ofsDirectoryPath());
this.directoryPath = new String(io.readBytesTerm((byte) 0, false, true, true), StandardCharsets.US_ASCII);
io.seek(_pos);
return this.directoryPath;
}
public void setDirectoryPath(String _v) { _dirty = true; directoryPath = _v; }
public void setDirectoryPath_Enabled(boolean _v) { _dirty = true; _enabledDirectoryPath = _v; }
private void _writeDirectoryPath() {
_shouldWriteDirectoryPath = false;
KaitaiStream io = _parent().nameTable()._io();
long _pos = io.pos();
io.seek(ofsDirectoryPath());
io.writeBytes((this.directoryPath).getBytes(Charset.forName("ASCII")));
io.writeU1(0);
io.seek(_pos);
}
private String fileName;
private boolean _shouldWriteFileName = false;
private boolean _enabledFileName = true;
public String fileName() {
if (_shouldWriteFileName)
_writeFileName();
if (this.fileName != null)
return this.fileName;
if (!_enabledFileName)
return null;
KaitaiStream io = _parent().nameTable()._io();
long _pos = io.pos();
io.seek(ofsFileName());
this.fileName = new String(io.readBytesTerm((byte) 0, false, true, true), StandardCharsets.US_ASCII);
io.seek(_pos);
return this.fileName;
}
public void setFileName(String _v) { _dirty = true; fileName = _v; }
public void setFileName_Enabled(boolean _v) { _dirty = true; _enabledFileName = _v; }
private void _writeFileName() {
_shouldWriteFileName = false;
KaitaiStream io = _parent().nameTable()._io();
long _pos = io.pos();
io.seek(ofsFileName());
io.writeBytes((this.fileName).getBytes(Charset.forName("ASCII")));
io.writeU1(0);
io.seek(_pos);
}
private long ofsDirectoryPath;
private long ofsFileName;
private long lzmaBlob;
private long ofsFile;
private long lenFile;
private long prevMaxLzmaBlob;
private TplinkMinifs _root;
private TplinkMinifs.BodyV3 _parent;
public long ofsDirectoryPath() { return ofsDirectoryPath; }
public void setOfsDirectoryPath(long _v) { _dirty = true; ofsDirectoryPath = _v; }
public long ofsFileName() { return ofsFileName; }
public void setOfsFileName(long _v) { _dirty = true; ofsFileName = _v; }
public long lzmaBlob() { return lzmaBlob; }
public void setLzmaBlob(long _v) { _dirty = true; lzmaBlob = _v; }
public long ofsFile() { return ofsFile; }
public void setOfsFile(long _v) { _dirty = true; ofsFile = _v; }
public long lenFile() { return lenFile; }
public void setLenFile(long _v) { _dirty = true; lenFile = _v; }
public long prevMaxLzmaBlob() { return prevMaxLzmaBlob; }
public void setPrevMaxLzmaBlob(long _v) { _dirty = true; prevMaxLzmaBlob = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs.BodyV3 _parent() { return _parent; }
public void set_parent(TplinkMinifs.BodyV3 _v) { _dirty = true; _parent = _v; }
}
public static class InodeV1 extends KaitaiStruct.ReadWrite {
public InodeV1(long prevOfsEnd) {
this(null, null, null, prevOfsEnd);
}
public InodeV1(KaitaiStream _io, long prevOfsEnd) {
this(_io, null, null, prevOfsEnd);
}
public InodeV1(KaitaiStream _io, TplinkMinifs.BodyV1 _parent, long prevOfsEnd) {
this(_io, _parent, null, prevOfsEnd);
}
public InodeV1(KaitaiStream _io, TplinkMinifs.BodyV1 _parent, TplinkMinifs _root, long prevOfsEnd) {
super(_io);
this._parent = _parent;
this._root = _root;
this.prevOfsEnd = prevOfsEnd;
}
public void _read() {
this.path = new String(KaitaiStream.bytesTerminate(this._io.readBytes(80), (byte) 0, false), StandardCharsets.US_ASCII);
this.lenBlob = this._io.readU4be();
this.ofsBlob = this._io.readU4be();
if (!(this.ofsBlob == prevOfsEnd())) {
throw new KaitaiStream.ValidationNotEqualError(prevOfsEnd(), this.ofsBlob, this._io, "/types/inode_v1/seq/2");
}
_dirty = false;
}
public void _fetchInstances() {
blob();
if (this.blob != null) {
}
}
public void _write_Seq() {
_assertNotDirty();
_shouldWriteBlob = _enabledBlob;
this._io.writeBytesLimit((this.path).getBytes(Charset.forName("ASCII")), 80, (byte) 0, (byte) 0);
this._io.writeU4be(this.lenBlob);
this._io.writeU4be(this.ofsBlob);
}
public void _check() {
if ((this.path).getBytes(Charset.forName("ASCII")).length > 80)
throw new ConsistencyError("path", 80, (this.path).getBytes(Charset.forName("ASCII")).length);
if (KaitaiStream.byteArrayIndexOf((this.path).getBytes(Charset.forName("ASCII")), ((byte) 0)) != -1)
throw new ConsistencyError("path", -1, KaitaiStream.byteArrayIndexOf((this.path).getBytes(Charset.forName("ASCII")), ((byte) 0)));
if (!(this.ofsBlob == prevOfsEnd())) {
throw new KaitaiStream.ValidationNotEqualError(prevOfsEnd(), this.ofsBlob, null, "/types/inode_v1/seq/2");
}
if (_enabledBlob) {
if (this.blob.length != lenBlob())
throw new ConsistencyError("blob", lenBlob(), this.blob.length);
}
_dirty = false;
}
private byte[] blob;
private boolean _shouldWriteBlob = false;
private boolean _enabledBlob = true;
/**
* An LZMA stream in the `.lzma` format (also known as LZMA_Alone) with
* the contents of the file. Decompress it in the same way as
* `<lzma_meta>.blob`.
*
* In all images examined, the `.lzma` header specifies the properties
* lc = 0, lp = 0, pb = 2 and an 8 MiB dictionary, so it starts with the
* bytes `5A 00 00 80 00`. It also specifies the uncompressed size of the
* stream, and there is no end-of-payload marker.
*/
public byte[] blob() {
if (_shouldWriteBlob)
_writeBlob();
if (this.blob != null)
return this.blob;
if (!_enabledBlob)
return null;
KaitaiStream io = _parent().blobsArea()._io();
long _pos = io.pos();
io.seek(ofsBlob());
this.blob = io.readBytes(lenBlob());
io.seek(_pos);
return this.blob;
}
public void setBlob(byte[] _v) { _dirty = true; blob = _v; }
public void setBlob_Enabled(boolean _v) { _dirty = true; _enabledBlob = _v; }
private void _writeBlob() {
_shouldWriteBlob = false;
KaitaiStream io = _parent().blobsArea()._io();
long _pos = io.pos();
io.seek(ofsBlob());
io.writeBytes(this.blob);
io.seek(_pos);
}
private Integer ofsEnd;
public Integer ofsEnd() {
if (this.ofsEnd != null)
return this.ofsEnd;
this.ofsEnd = ((Number) (ofsBlob() + lenBlob())).intValue();
return this.ofsEnd;
}
public void _invalidateOfsEnd() { this.ofsEnd = null; }
private String path;
private long lenBlob;
private long ofsBlob;
private long prevOfsEnd;
private TplinkMinifs _root;
private TplinkMinifs.BodyV1 _parent;
public String path() { return path; }
public void setPath(String _v) { _dirty = true; path = _v; }
public long lenBlob() { return lenBlob; }
public void setLenBlob(long _v) { _dirty = true; lenBlob = _v; }
/**
* Relative to the start of `<body_v1>.blobs_area`.
*/
public long ofsBlob() { return ofsBlob; }
public void setOfsBlob(long _v) { _dirty = true; ofsBlob = _v; }
public long prevOfsEnd() { return prevOfsEnd; }
public void setPrevOfsEnd(long _v) { _dirty = true; prevOfsEnd = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs.BodyV1 _parent() { return _parent; }
public void set_parent(TplinkMinifs.BodyV1 _v) { _dirty = true; _parent = _v; }
}
/**
* @see <a href="https://arxiv.org/html/2407.05064v1#S4.SS4">Source</a>
* @see <a href="https://github.com/onekey-sec/unblob/blob/678dbce13dcabe3b4ff16b2b958d6f2cfce9ae3f/python/unblob/handlers/filesystem/minifs.py#L44-L48">Source</a>
*/
public static class LzmaMeta extends KaitaiStruct.ReadWrite {
public LzmaMeta(long prevOfsEnd) {
this(null, null, null, prevOfsEnd);
}
public LzmaMeta(KaitaiStream _io, long prevOfsEnd) {
this(_io, null, null, prevOfsEnd);
}
public LzmaMeta(KaitaiStream _io, TplinkMinifs.BodyV3 _parent, long prevOfsEnd) {
this(_io, _parent, null, prevOfsEnd);
}
public LzmaMeta(KaitaiStream _io, TplinkMinifs.BodyV3 _parent, TplinkMinifs _root, long prevOfsEnd) {
super(_io);
this._parent = _parent;
this._root = _root;
this.prevOfsEnd = prevOfsEnd;
}
public void _read() {
this.ofsBlob = this._io.readU4be();
if (!(this.ofsBlob == prevOfsEnd())) {
throw new KaitaiStream.ValidationNotEqualError(prevOfsEnd(), this.ofsBlob, this._io, "/types/lzma_meta/seq/0");
}
this.lenBlob = this._io.readU4be();
if (!(_parent().has8ByteLzmaMetas())) {
this.lenBlobUncompressed = this._io.readU4be();
}
_dirty = false;
}
public void _fetchInstances() {
if (!(_parent().has8ByteLzmaMetas())) {
}
blob();
if (this.blob != null) {
}
}
public void _write_Seq() {
_assertNotDirty();
_shouldWriteBlob = _enabledBlob;
this._io.writeU4be(this.ofsBlob);
this._io.writeU4be(this.lenBlob);
if (!(_parent().has8ByteLzmaMetas())) {
this._io.writeU4be(this.lenBlobUncompressed);
}
}
public void _check() {
if (!(this.ofsBlob == prevOfsEnd())) {
throw new KaitaiStream.ValidationNotEqualError(prevOfsEnd(), this.ofsBlob, null, "/types/lzma_meta/seq/0");
}
if (!(_parent().has8ByteLzmaMetas())) {
}
if (_enabledBlob) {
if (this.blob.length != lenBlob())
throw new ConsistencyError("blob", lenBlob(), this.blob.length);
}
_dirty = false;
}
private byte[] blob;
private boolean _shouldWriteBlob = false;
private boolean _enabledBlob = true;
/**
* An LZMA stream in the `.lzma` format (also known as LZMA_Alone):
* a 13-byte header followed by the compressed data. Decompress it
* with a ready-made LZMA decoder that reads this header, e.g. in
* Python with `lzma.decompress(blob, format=lzma.FORMAT_ALONE)`. The
* size of the decompressed data is `len_blob_uncompressed`, if present.
*
* In all images examined with `header.version` = 3, the `.lzma` header
* specifies the properties lc = 3, lp = 0, pb = 2 and an 8 MiB
* dictionary, so it starts with the bytes `5D 00 00 80 00`. The
* uncompressed size in the header is unknown (`0xFFFF_FFFF_FFFF_FFFF`),
* so the stream ends with an end-of-payload marker.
* @see <a href="https://arxiv.org/html/2407.05064v1#S4.SS5">Source</a>
* @see <a href="https://github.com/onekey-sec/unblob/blob/678dbce13dcabe3b4ff16b2b958d6f2cfce9ae3f/python/unblob/handlers/filesystem/minifs.py#L175-L187">Source</a>
*/
public byte[] blob() {
if (_shouldWriteBlob)
_writeBlob();
if (this.blob != null)
return this.blob;
if (!_enabledBlob)
return null;
KaitaiStream io = _parent().lzmaBlobsArea()._io();
long _pos = io.pos();
io.seek(ofsBlob());
this.blob = io.readBytes(lenBlob());
io.seek(_pos);
return this.blob;
}
public void setBlob(byte[] _v) { _dirty = true; blob = _v; }
public void setBlob_Enabled(boolean _v) { _dirty = true; _enabledBlob = _v; }
private void _writeBlob() {
_shouldWriteBlob = false;
KaitaiStream io = _parent().lzmaBlobsArea()._io();
long _pos = io.pos();
io.seek(ofsBlob());
io.writeBytes(this.blob);
io.seek(_pos);
}
private Integer ofsEnd;
public Integer ofsEnd() {
if (this.ofsEnd != null)
return this.ofsEnd;
this.ofsEnd = ((Number) (ofsBlob() + lenBlob())).intValue();
return this.ofsEnd;
}
public void _invalidateOfsEnd() { this.ofsEnd = null; }
private long ofsBlob;
private long lenBlob;
private Long lenBlobUncompressed;
private long prevOfsEnd;
private TplinkMinifs _root;
private TplinkMinifs.BodyV3 _parent;
public long ofsBlob() { return ofsBlob; }
public void setOfsBlob(long _v) { _dirty = true; ofsBlob = _v; }
public long lenBlob() { return lenBlob; }
public void setLenBlob(long _v) { _dirty = true; lenBlob = _v; }
public Long lenBlobUncompressed() { return lenBlobUncompressed; }
public void setLenBlobUncompressed(Long _v) { _dirty = true; lenBlobUncompressed = _v; }
public long prevOfsEnd() { return prevOfsEnd; }
public void setPrevOfsEnd(long _v) { _dirty = true; prevOfsEnd = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs.BodyV3 _parent() { return _parent; }
public void set_parent(TplinkMinifs.BodyV3 _v) { _dirty = true; _parent = _v; }
}
/**
* @see <a href="https://arxiv.org/html/2407.05064v1#S4.SS1">Source</a>
* @see <a href="https://github.com/onekey-sec/unblob/blob/678dbce13dcabe3b4ff16b2b958d6f2cfce9ae3f/python/unblob/handlers/filesystem/minifs.py#L27-L34">Source</a>
*/
public static class MinifsHeader extends KaitaiStruct.ReadWrite {
public static MinifsHeader fromFile(String fileName) throws IOException {
return new MinifsHeader(new ByteBufferKaitaiStream(fileName));
}
public MinifsHeader() {
this(null, null, null);
}
public MinifsHeader(KaitaiStream _io) {
this(_io, null, null);
}
public MinifsHeader(KaitaiStream _io, TplinkMinifs _parent) {
this(_io, _parent, null);
}
public MinifsHeader(KaitaiStream _io, TplinkMinifs _parent, TplinkMinifs _root) {
super(_io);
this._parent = _parent;
this._root = _root;
}
public void _read() {
this.magic = this._io.readBytes(16);
if (!(Arrays.equals(this.magic, new byte[] { 77, 73, 78, 73, 70, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }))) {
throw new KaitaiStream.ValidationNotEqualError(new byte[] { 77, 73, 78, 73, 70, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, this.magic, this._io, "/types/minifs_header/seq/0");
}
this.version = this._io.readU4be();
if (!( ((this.version == 1) || (this.version == 2) || (this.version == 3)) )) {
throw new KaitaiStream.ValidationNotAnyOfError(this.version, this._io, "/types/minifs_header/seq/1");
}
this.numFiles = this._io.readU4be();
this.lenLargestFile = this._io.readU4be();
_dirty = false;
}
public void _fetchInstances() {
}
public void _write_Seq() {
_assertNotDirty();
this._io.writeBytes(this.magic);
this._io.writeU4be(this.version);
this._io.writeU4be(this.numFiles);
this._io.writeU4be(this.lenLargestFile);
}
public void _check() {
if (this.magic.length != 16)
throw new ConsistencyError("magic", 16, this.magic.length);
if (!(Arrays.equals(this.magic, new byte[] { 77, 73, 78, 73, 70, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }))) {
throw new KaitaiStream.ValidationNotEqualError(new byte[] { 77, 73, 78, 73, 70, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, this.magic, null, "/types/minifs_header/seq/0");
}
if (!( ((this.version == 1) || (this.version == 2) || (this.version == 3)) )) {
throw new KaitaiStream.ValidationNotAnyOfError(this.version, null, "/types/minifs_header/seq/1");
}
_dirty = false;
}
private byte[] magic;
private long version;
private long numFiles;
private long lenLargestFile;
private TplinkMinifs _root;
private TplinkMinifs _parent;
public byte[] magic() { return magic; }
public void setMagic(byte[] _v) { _dirty = true; magic = _v; }
public long version() { return version; }
public void setVersion(long _v) { _dirty = true; version = _v; }
public long numFiles() { return numFiles; }
public void setNumFiles(long _v) { _dirty = true; numFiles = _v; }
/**
* The uncompressed size of the largest file. In all images examined with
* version 3, the inodes are sorted by size in descending order, so it is
* also the size of the first file. In all images examined with versions
* 1 and 2, the value is rounded up to a multiple of 4.
*/
public long lenLargestFile() { return lenLargestFile; }
public void setLenLargestFile(long _v) { _dirty = true; lenLargestFile = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs _parent() { return _parent; }
public void set_parent(TplinkMinifs _v) { _dirty = true; _parent = _v; }
}
/**
* Table of null-terminated directory paths and file names, referenced by
* `<inode>.ofs_directory_path` and `<inode>.ofs_file_name`. In all images
* examined, each non-empty string occurs only once, which saves space in
* flash memory, as many files share a directory or a name.
*
* In all images examined, the table is padded with null bytes to a multiple
* of 4 bytes, so `names` can end with up to 3 empty strings (one for each
* padding byte).
* @see <a href="https://arxiv.org/html/2407.05064v1#S4.SS2">Source</a>
*/
public static class NameTable extends KaitaiStruct.ReadWrite {
public static NameTable fromFile(String fileName) throws IOException {
return new NameTable(new ByteBufferKaitaiStream(fileName));
}
public NameTable() {
this(null, null, null);
}
public NameTable(KaitaiStream _io) {
this(_io, null, null);
}
public NameTable(KaitaiStream _io, TplinkMinifs.BodyV3 _parent) {
this(_io, _parent, null);
}
public NameTable(KaitaiStream _io, TplinkMinifs.BodyV3 _parent, TplinkMinifs _root) {
super(_io);
this._parent = _parent;
this._root = _root;
}
public void _read() {
this.names = new ArrayList<String>();
{
int i = 0;
while (!this._io.isEof()) {
this.names.add(new String(this._io.readBytesTerm((byte) 0, false, true, true), StandardCharsets.US_ASCII));
i++;
}
}
_dirty = false;
}
public void _fetchInstances() {
for (int i = 0; i < this.names.size(); i++) {
}
}
public void _write_Seq() {
_assertNotDirty();
for (int i = 0; i < this.names.size(); i++) {
if (this._io.isEof())
throw new ConsistencyError("names", 0, this._io.size() - this._io.pos());
this._io.writeBytes((this.names.get(((Number) (i)).intValue())).getBytes(Charset.forName("ASCII")));
this._io.writeU1(0);
}
if (!(this._io.isEof()))
throw new ConsistencyError("names", 0, this._io.size() - this._io.pos());
}
public void _check() {
for (int i = 0; i < this.names.size(); i++) {
if (KaitaiStream.byteArrayIndexOf((this.names.get(((Number) (i)).intValue())).getBytes(Charset.forName("ASCII")), ((byte) 0)) != -1)
throw new ConsistencyError("names", -1, KaitaiStream.byteArrayIndexOf((this.names.get(((Number) (i)).intValue())).getBytes(Charset.forName("ASCII")), ((byte) 0)));
}
_dirty = false;
}
private List<String> names;
private TplinkMinifs _root;
private TplinkMinifs.BodyV3 _parent;
public List<String> names() { return names; }
public void setNames(List<String> _v) { _dirty = true; names = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public TplinkMinifs.BodyV3 _parent() { return _parent; }
public void set_parent(TplinkMinifs.BodyV3 _v) { _dirty = true; _parent = _v; }
}
private MinifsHeader header;
private KaitaiStruct.ReadWrite body;
private TplinkMinifs _root;
private KaitaiStruct.ReadWrite _parent;
public MinifsHeader header() { return header; }
public void setHeader(MinifsHeader _v) { _dirty = true; header = _v; }
public KaitaiStruct.ReadWrite body() { return body; }
public void setBody(KaitaiStruct.ReadWrite _v) { _dirty = true; body = _v; }
public TplinkMinifs _root() { return _root; }
public void set_root(TplinkMinifs _v) { _dirty = true; _root = _v; }
public KaitaiStruct.ReadWrite _parent() { return _parent; }
public void set_parent(KaitaiStruct.ReadWrite _v) { _dirty = true; _parent = _v; }
}