TP-Link MiniFS: Go parsing library

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.

KS implementation details

License: CC0-1.0

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.

Go source code to parse TP-Link MiniFS

tplink_minifs.go

// Code generated by kaitai-struct-compiler from a .ksy source file. DO NOT EDIT.

import (
	"github.com/kaitai-io/kaitai_struct_go_runtime/kaitai"
	"bytes"
	"io"
)


/**
 * 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>
 */

type TplinkMinifs_CompressionType int
const (
	TplinkMinifs_CompressionType__Lzma TplinkMinifs_CompressionType = 0
	TplinkMinifs_CompressionType__Raw TplinkMinifs_CompressionType = 2
	TplinkMinifs_CompressionType__Lzma2014 TplinkMinifs_CompressionType = 4
)
var values_TplinkMinifs_CompressionType = map[TplinkMinifs_CompressionType]struct{}{0: {}, 2: {}, 4: {}}
func (v TplinkMinifs_CompressionType) isDefined() bool {
	_, ok := values_TplinkMinifs_CompressionType[v]
	return ok
}
type TplinkMinifs struct {
	Header *TplinkMinifs_MinifsHeader
	Body kaitai.Struct
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent kaitai.Struct
}
func NewTplinkMinifs() *TplinkMinifs {
	return &TplinkMinifs{
	}
}

func (this TplinkMinifs) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs) Read(io *kaitai.Stream, parent kaitai.Struct, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp1 := NewTplinkMinifs_MinifsHeader()
	err = tmp1.Read(this._io, this, this._root)
	if err != nil {
		return err
	}
	this.Header = tmp1
	switch (this.Header.Version) {
	case 1:
		tmp2 := NewTplinkMinifs_BodyV1()
		err = tmp2.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.Body = tmp2
	case 2:
		tmp3 := NewTplinkMinifs_BodyV2()
		err = tmp3.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.Body = tmp3
	case 3:
		tmp4 := NewTplinkMinifs_BodyV3()
		err = tmp4.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.Body = tmp4
	}
	return err
}

/**
 * @see <a href="https://arxiv.org/html/2407.05064v1#S2.SS2">Source</a>
 */
type TplinkMinifs_BodyV1 struct {
	Inodes []*TplinkMinifs_InodeV1
	BlobsArea *TplinkMinifs_Dummy
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs
	_raw_BlobsArea []byte
}
func NewTplinkMinifs_BodyV1() *TplinkMinifs_BodyV1 {
	return &TplinkMinifs_BodyV1{
	}
}

func (this TplinkMinifs_BodyV1) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_BodyV1) Read(io *kaitai.Stream, parent *TplinkMinifs, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	for i := 0; i < int(this._root.Header.NumFiles); i++ {
		_ = i
		var tmp5 int;
		if (i != 0) {
			tmp6, err := this.Inodes[i - 1].OfsEnd()
			if err != nil {
				return err
			}
			tmp5 = tmp6
		} else {
			tmp5 = 0
		}
		tmp7 := NewTplinkMinifs_InodeV1(uint32(tmp5))
		err = tmp7.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.Inodes = append(this.Inodes, tmp7)
	}
	tmp8 := this.Inodes
	tmp9, err := tmp8[len(tmp8) - 1].OfsEnd()
	if err != nil {
		return err
	}
	tmp10, err := this._io.ReadBytes(int(tmp9))
	if err != nil {
		return err
	}
	tmp10 = tmp10
	this._raw_BlobsArea = tmp10
	_io__raw_BlobsArea := kaitai.NewStream(bytes.NewReader(this._raw_BlobsArea))
	tmp11 := NewTplinkMinifs_Dummy()
	err = tmp11.Read(_io__raw_BlobsArea, this, this._root)
	if err != nil {
		return err
	}
	this.BlobsArea = tmp11
	return err
}
type TplinkMinifs_BodyV2 struct {
	LenFilesArea uint32
	FilesArea *TplinkMinifs_FilesAreaV2
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs
	_raw_FilesArea []byte
}
func NewTplinkMinifs_BodyV2() *TplinkMinifs_BodyV2 {
	return &TplinkMinifs_BodyV2{
	}
}

func (this TplinkMinifs_BodyV2) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_BodyV2) Read(io *kaitai.Stream, parent *TplinkMinifs, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp12, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.LenFilesArea = uint32(tmp12)
	tmp13, err := this._io.ReadBytes(int(this.LenFilesArea))
	if err != nil {
		return err
	}
	tmp13 = tmp13
	this._raw_FilesArea = tmp13
	_io__raw_FilesArea := kaitai.NewStream(bytes.NewReader(this._raw_FilesArea))
	tmp14 := NewTplinkMinifs_FilesAreaV2()
	err = tmp14.Read(_io__raw_FilesArea, this, this._root)
	if err != nil {
		return err
	}
	this.FilesArea = tmp14
	return err
}
type TplinkMinifs_BodyV3 struct {
	LenNameTable uint32
	NameTable *TplinkMinifs_NameTable
	Inodes []*TplinkMinifs_Inode
	_unnamed3 []byte
	LzmaMetas []*TplinkMinifs_LzmaMeta
	LzmaBlobsArea *TplinkMinifs_Dummy
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs
	_raw_NameTable []byte
	_raw_LzmaBlobsArea []byte
	_f_has8ByteLzmaMetas bool
	has8ByteLzmaMetas bool
	_f_lenFirstBlobUncompressedIf8ByteMetas bool
	lenFirstBlobUncompressedIf8ByteMetas uint64
	_f_lenLzmaMeta bool
	lenLzmaMeta int8
	_f_numLzmaBlobs bool
	numLzmaBlobs int
	_f_ofsLzmaMetas bool
	ofsLzmaMetas int
}
func NewTplinkMinifs_BodyV3() *TplinkMinifs_BodyV3 {
	return &TplinkMinifs_BodyV3{
	}
}

func (this TplinkMinifs_BodyV3) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_BodyV3) Read(io *kaitai.Stream, parent *TplinkMinifs, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp15, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.LenNameTable = uint32(tmp15)
	tmp16, err := this._io.ReadBytes(int(this.LenNameTable))
	if err != nil {
		return err
	}
	tmp16 = tmp16
	this._raw_NameTable = tmp16
	_io__raw_NameTable := kaitai.NewStream(bytes.NewReader(this._raw_NameTable))
	tmp17 := NewTplinkMinifs_NameTable()
	err = tmp17.Read(_io__raw_NameTable, this, this._root)
	if err != nil {
		return err
	}
	this.NameTable = tmp17
	for i := 0; i < int(this._root.Header.NumFiles); i++ {
		_ = i
		var tmp18 int;
		if (i != 0) {
			tmp19, err := this.Inodes[i - 1].CurMaxLzmaBlob()
			if err != nil {
				return err
			}
			tmp18 = tmp19
		} else {
			tmp18 = 0
		}
		tmp20 := NewTplinkMinifs_Inode(uint32(tmp18))
		err = tmp20.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.Inodes = append(this.Inodes, tmp20)
	}
	tmp21, err := this.OfsLzmaMetas()
	if err != nil {
		return err
	}
	if (tmp21 < 0) {
		tmp22, err := this._io.ReadBytes(int(0))
		if err != nil {
			return err
		}
		tmp22 = tmp22
		this._unnamed3 = tmp22
	}
	tmp23, err := this.NumLzmaBlobs()
	if err != nil {
		return err
	}
	for i := 0; i < int(tmp23); i++ {
		_ = i
		var tmp24 int;
		if (i != 0) {
			tmp25, err := this.LzmaMetas[i - 1].OfsEnd()
			if err != nil {
				return err
			}
			tmp24 = tmp25
		} else {
			tmp24 = 0
		}
		tmp26 := NewTplinkMinifs_LzmaMeta(uint32(tmp24))
		err = tmp26.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.LzmaMetas = append(this.LzmaMetas, tmp26)
	}
	tmp27 := this.LzmaMetas
	tmp28, err := tmp27[len(tmp27) - 1].OfsEnd()
	if err != nil {
		return err
	}
	tmp29, err := this._io.ReadBytes(int(tmp28))
	if err != nil {
		return err
	}
	tmp29 = tmp29
	this._raw_LzmaBlobsArea = tmp29
	_io__raw_LzmaBlobsArea := kaitai.NewStream(bytes.NewReader(this._raw_LzmaBlobsArea))
	tmp30 := NewTplinkMinifs_Dummy()
	err = tmp30.Read(_io__raw_LzmaBlobsArea, this, this._root)
	if err != nil {
		return err
	}
	this.LzmaBlobsArea = tmp30
	return err
}

/**
 * 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`.
 */
func (this *TplinkMinifs_BodyV3) Has8ByteLzmaMetas() (v bool, err error) {
	if (this._f_has8ByteLzmaMetas) {
		return this.has8ByteLzmaMetas, nil
	}
	this._f_has8ByteLzmaMetas = true
	tmp31, err := this.LenFirstBlobUncompressedIf8ByteMetas()
	if err != nil {
		return false, err
	}
	this.has8ByteLzmaMetas = bool(tmp31 == uint64(18446744073709551615))
	return this.has8ByteLzmaMetas, nil
}

/**
 * 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.
 */
func (this *TplinkMinifs_BodyV3) LenFirstBlobUncompressedIf8ByteMetas() (v uint64, err error) {
	if (this._f_lenFirstBlobUncompressedIf8ByteMetas) {
		return this.lenFirstBlobUncompressedIf8ByteMetas, nil
	}
	this._f_lenFirstBlobUncompressedIf8ByteMetas = true
	_pos, err := this._io.Pos()
	if err != nil {
		return 0, err
	}
	tmp32, err := this.OfsLzmaMetas()
	if err != nil {
		return 0, err
	}
	tmp33, err := this.NumLzmaBlobs()
	if err != nil {
		return 0, err
	}
	_, err = this._io.Seek(int64((tmp32 + 8 * tmp33) + 5), io.SeekStart)
	if err != nil {
		return 0, err
	}
	tmp34, err := this._io.ReadU8le()
	if err != nil {
		return 0, err
	}
	this.lenFirstBlobUncompressedIf8ByteMetas = tmp34
	_, err = this._io.Seek(_pos, io.SeekStart)
	if err != nil {
		return 0, err
	}
	return this.lenFirstBlobUncompressedIf8ByteMetas, nil
}

/**
 * The size of each record in `lzma_metas` (8 or 12 bytes).
 */
func (this *TplinkMinifs_BodyV3) LenLzmaMeta() (v int8, err error) {
	if (this._f_lenLzmaMeta) {
		return this.lenLzmaMeta, nil
	}
	this._f_lenLzmaMeta = true
	var tmp35 int8;
	tmp36, err := this.Has8ByteLzmaMetas()
	if err != nil {
		return 0, err
	}
	if (tmp36) {
		tmp35 = 8
	} else {
		tmp35 = 12
	}
	this.lenLzmaMeta = int8(tmp35)
	return this.lenLzmaMeta, nil
}

/**
 * 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>
 */
func (this *TplinkMinifs_BodyV3) NumLzmaBlobs() (v int, err error) {
	if (this._f_numLzmaBlobs) {
		return this.numLzmaBlobs, nil
	}
	this._f_numLzmaBlobs = true
	tmp37 := this.Inodes
	tmp38, err := tmp37[len(tmp37) - 1].CurMaxLzmaBlob()
	if err != nil {
		return 0, err
	}
	this.numLzmaBlobs = int(tmp38 + 1)
	return this.numLzmaBlobs, nil
}
func (this *TplinkMinifs_BodyV3) OfsLzmaMetas() (v int, err error) {
	if (this._f_ofsLzmaMetas) {
		return this.ofsLzmaMetas, nil
	}
	this._f_ofsLzmaMetas = true
	tmp39, err := this._io.Pos()
	if err != nil {
		return 0, err
	}
	this.ofsLzmaMetas = int(tmp39)
	return this.ofsLzmaMetas, nil
}
type TplinkMinifs_Dummy struct {
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent kaitai.Struct
}
func NewTplinkMinifs_Dummy() *TplinkMinifs_Dummy {
	return &TplinkMinifs_Dummy{
	}
}

func (this TplinkMinifs_Dummy) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_Dummy) Read(io *kaitai.Stream, parent kaitai.Struct, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	return err
}

/**
 * @see <a href="https://github.com/zh-explorer/tp_research/blob/a42f9d0427ad0c453a7060f3236c4174bb883441/Minifs.py#L41-L52">Source</a>
 */
type TplinkMinifs_FileV2 struct {
	PathMd5 []byte
	LenData uint32
	OfsData uint32
	LenDataUncompressed uint32
	Compression TplinkMinifs_CompressionType
	Data []byte
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs_FilesAreaV2
}
func NewTplinkMinifs_FileV2() *TplinkMinifs_FileV2 {
	return &TplinkMinifs_FileV2{
	}
}

func (this TplinkMinifs_FileV2) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_FileV2) Read(io *kaitai.Stream, parent *TplinkMinifs_FilesAreaV2, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp40, err := this._io.ReadBytes(int(16))
	if err != nil {
		return err
	}
	tmp40 = tmp40
	this.PathMd5 = tmp40
	tmp41, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.LenData = uint32(tmp41)
	tmp42, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.OfsData = uint32(tmp42)
	tmp43, err := this._io.Pos()
	if err != nil {
		return err
	}
	tmp44, err := this._io.Pos()
	if err != nil {
		return err
	}
	if !(this.OfsData == tmp43 + 8) {
		return kaitai.NewValidationNotEqualError(tmp44 + 8, this.OfsData, this._io, "/types/file_v2/seq/2")
	}
	tmp45, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.LenDataUncompressed = uint32(tmp45)
	tmp46, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.Compression = TplinkMinifs_CompressionType(tmp46)
	tmp47, err := this._io.ReadBytes(int(this.LenData))
	if err != nil {
		return err
	}
	tmp47 = tmp47
	this.Data = tmp47
	return err
}

/**
 * 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.
 */

/**
 * Offset of `data` relative to the start of `<body_v2>.files_area`.
 */

/**
 * 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.
 */
type TplinkMinifs_FilesAreaV2 struct {
	Files []*TplinkMinifs_FileV2
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs_BodyV2
}
func NewTplinkMinifs_FilesAreaV2() *TplinkMinifs_FilesAreaV2 {
	return &TplinkMinifs_FilesAreaV2{
	}
}

func (this TplinkMinifs_FilesAreaV2) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_FilesAreaV2) Read(io *kaitai.Stream, parent *TplinkMinifs_BodyV2, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	for i := 0; i < int(this._root.Header.NumFiles); i++ {
		_ = i
		tmp48 := NewTplinkMinifs_FileV2()
		err = tmp48.Read(this._io, this, this._root)
		if err != nil {
			return err
		}
		this.Files = append(this.Files, tmp48)
	}
	return err
}

/**
 * @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>
 */
type TplinkMinifs_Inode struct {
	OfsDirectoryPath uint32
	OfsFileName uint32
	LzmaBlob uint32
	OfsFile uint32
	LenFile uint32
	PrevMaxLzmaBlob uint32
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs_BodyV3
	_f_curMaxLzmaBlob bool
	curMaxLzmaBlob int
	_f_directoryPath bool
	directoryPath string
	_f_fileName bool
	fileName string
}
func NewTplinkMinifs_Inode(prevMaxLzmaBlob uint32) *TplinkMinifs_Inode {
	return &TplinkMinifs_Inode{
		PrevMaxLzmaBlob: prevMaxLzmaBlob,
	}
}

func (this TplinkMinifs_Inode) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_Inode) Read(io *kaitai.Stream, parent *TplinkMinifs_BodyV3, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp49, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.OfsDirectoryPath = uint32(tmp49)
	tmp50, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.OfsFileName = uint32(tmp50)
	tmp51, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.LzmaBlob = uint32(tmp51)
	tmp52, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.OfsFile = uint32(tmp52)
	tmp53, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.LenFile = uint32(tmp53)
	return err
}
func (this *TplinkMinifs_Inode) CurMaxLzmaBlob() (v int, err error) {
	if (this._f_curMaxLzmaBlob) {
		return this.curMaxLzmaBlob, nil
	}
	this._f_curMaxLzmaBlob = true
	var tmp54 uint32;
	if (this.LzmaBlob > this.PrevMaxLzmaBlob) {
		tmp54 = this.LzmaBlob
	} else {
		tmp54 = this.PrevMaxLzmaBlob
	}
	this.curMaxLzmaBlob = int(tmp54)
	return this.curMaxLzmaBlob, nil
}
func (this *TplinkMinifs_Inode) DirectoryPath() (v string, err error) {
	if (this._f_directoryPath) {
		return this.directoryPath, nil
	}
	this._f_directoryPath = true
	thisIo := this._parent.NameTable._io
	_pos, err := thisIo.Pos()
	if err != nil {
		return "", err
	}
	_, err = thisIo.Seek(int64(this.OfsDirectoryPath), io.SeekStart)
	if err != nil {
		return "", err
	}
	tmp55, err := thisIo.ReadBytesTerm(0, false, true, true)
	if err != nil {
		return "", err
	}
	this.directoryPath = string(tmp55)
	_, err = thisIo.Seek(_pos, io.SeekStart)
	if err != nil {
		return "", err
	}
	return this.directoryPath, nil
}
func (this *TplinkMinifs_Inode) FileName() (v string, err error) {
	if (this._f_fileName) {
		return this.fileName, nil
	}
	this._f_fileName = true
	thisIo := this._parent.NameTable._io
	_pos, err := thisIo.Pos()
	if err != nil {
		return "", err
	}
	_, err = thisIo.Seek(int64(this.OfsFileName), io.SeekStart)
	if err != nil {
		return "", err
	}
	tmp56, err := thisIo.ReadBytesTerm(0, false, true, true)
	if err != nil {
		return "", err
	}
	this.fileName = string(tmp56)
	_, err = thisIo.Seek(_pos, io.SeekStart)
	if err != nil {
		return "", err
	}
	return this.fileName, nil
}
type TplinkMinifs_InodeV1 struct {
	Path string
	LenBlob uint32
	OfsBlob uint32
	PrevOfsEnd uint32
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs_BodyV1
	_f_blob bool
	blob []byte
	_f_ofsEnd bool
	ofsEnd int
}
func NewTplinkMinifs_InodeV1(prevOfsEnd uint32) *TplinkMinifs_InodeV1 {
	return &TplinkMinifs_InodeV1{
		PrevOfsEnd: prevOfsEnd,
	}
}

func (this TplinkMinifs_InodeV1) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_InodeV1) Read(io *kaitai.Stream, parent *TplinkMinifs_BodyV1, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp57, err := this._io.ReadBytes(int(80))
	if err != nil {
		return err
	}
	tmp57 = kaitai.BytesTerminate(tmp57, 0, false)
	this.Path = string(tmp57)
	tmp58, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.LenBlob = uint32(tmp58)
	tmp59, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.OfsBlob = uint32(tmp59)
	if !(this.OfsBlob == this.PrevOfsEnd) {
		return kaitai.NewValidationNotEqualError(this.PrevOfsEnd, this.OfsBlob, this._io, "/types/inode_v1/seq/2")
	}
	return err
}

/**
 * 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.
 */
func (this *TplinkMinifs_InodeV1) Blob() (v []byte, err error) {
	if (this._f_blob) {
		return this.blob, nil
	}
	this._f_blob = true
	thisIo := this._parent.BlobsArea._io
	_pos, err := thisIo.Pos()
	if err != nil {
		return nil, err
	}
	_, err = thisIo.Seek(int64(this.OfsBlob), io.SeekStart)
	if err != nil {
		return nil, err
	}
	tmp60, err := thisIo.ReadBytes(int(this.LenBlob))
	if err != nil {
		return nil, err
	}
	tmp60 = tmp60
	this.blob = tmp60
	_, err = thisIo.Seek(_pos, io.SeekStart)
	if err != nil {
		return nil, err
	}
	return this.blob, nil
}
func (this *TplinkMinifs_InodeV1) OfsEnd() (v int, err error) {
	if (this._f_ofsEnd) {
		return this.ofsEnd, nil
	}
	this._f_ofsEnd = true
	this.ofsEnd = int(this.OfsBlob + this.LenBlob)
	return this.ofsEnd, nil
}

/**
 * Relative to the start of `<body_v1>.blobs_area`.
 */

/**
 * @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>
 */
type TplinkMinifs_LzmaMeta struct {
	OfsBlob uint32
	LenBlob uint32
	LenBlobUncompressed uint32
	PrevOfsEnd uint32
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs_BodyV3
	_f_blob bool
	blob []byte
	_f_ofsEnd bool
	ofsEnd int
}
func NewTplinkMinifs_LzmaMeta(prevOfsEnd uint32) *TplinkMinifs_LzmaMeta {
	return &TplinkMinifs_LzmaMeta{
		PrevOfsEnd: prevOfsEnd,
	}
}

func (this TplinkMinifs_LzmaMeta) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_LzmaMeta) Read(io *kaitai.Stream, parent *TplinkMinifs_BodyV3, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp61, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.OfsBlob = uint32(tmp61)
	if !(this.OfsBlob == this.PrevOfsEnd) {
		return kaitai.NewValidationNotEqualError(this.PrevOfsEnd, this.OfsBlob, this._io, "/types/lzma_meta/seq/0")
	}
	tmp62, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.LenBlob = uint32(tmp62)
	tmp63, err := this._parent.Has8ByteLzmaMetas()
	if err != nil {
		return err
	}
	if (!(tmp63)) {
		tmp64, err := this._io.ReadU4be()
		if err != nil {
			return err
		}
		this.LenBlobUncompressed = uint32(tmp64)
	}
	return err
}

/**
 * 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>
 */
func (this *TplinkMinifs_LzmaMeta) Blob() (v []byte, err error) {
	if (this._f_blob) {
		return this.blob, nil
	}
	this._f_blob = true
	thisIo := this._parent.LzmaBlobsArea._io
	_pos, err := thisIo.Pos()
	if err != nil {
		return nil, err
	}
	_, err = thisIo.Seek(int64(this.OfsBlob), io.SeekStart)
	if err != nil {
		return nil, err
	}
	tmp65, err := thisIo.ReadBytes(int(this.LenBlob))
	if err != nil {
		return nil, err
	}
	tmp65 = tmp65
	this.blob = tmp65
	_, err = thisIo.Seek(_pos, io.SeekStart)
	if err != nil {
		return nil, err
	}
	return this.blob, nil
}
func (this *TplinkMinifs_LzmaMeta) OfsEnd() (v int, err error) {
	if (this._f_ofsEnd) {
		return this.ofsEnd, nil
	}
	this._f_ofsEnd = true
	this.ofsEnd = int(this.OfsBlob + this.LenBlob)
	return this.ofsEnd, nil
}

/**
 * @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>
 */
type TplinkMinifs_MinifsHeader struct {
	Magic []byte
	Version uint32
	NumFiles uint32
	LenLargestFile uint32
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs
}
func NewTplinkMinifs_MinifsHeader() *TplinkMinifs_MinifsHeader {
	return &TplinkMinifs_MinifsHeader{
	}
}

func (this TplinkMinifs_MinifsHeader) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_MinifsHeader) Read(io *kaitai.Stream, parent *TplinkMinifs, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	tmp66, err := this._io.ReadBytes(int(16))
	if err != nil {
		return err
	}
	tmp66 = tmp66
	this.Magic = tmp66
	if !(bytes.Equal(this.Magic, []uint8{77, 73, 78, 73, 70, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})) {
		return kaitai.NewValidationNotEqualError([]uint8{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")
	}
	tmp67, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.Version = uint32(tmp67)
	if !( ((this.Version == 1) || (this.Version == 2) || (this.Version == 3)) ) {
		return kaitai.NewValidationNotAnyOfError(this.Version, this._io, "/types/minifs_header/seq/1")
	}
	tmp68, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.NumFiles = uint32(tmp68)
	tmp69, err := this._io.ReadU4be()
	if err != nil {
		return err
	}
	this.LenLargestFile = uint32(tmp69)
	return err
}

/**
 * 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.
 */

/**
 * 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>
 */
type TplinkMinifs_NameTable struct {
	Names []string
	_io *kaitai.Stream
	_root *TplinkMinifs
	_parent *TplinkMinifs_BodyV3
}
func NewTplinkMinifs_NameTable() *TplinkMinifs_NameTable {
	return &TplinkMinifs_NameTable{
	}
}

func (this TplinkMinifs_NameTable) IO_() *kaitai.Stream {
	return this._io
}

func (this *TplinkMinifs_NameTable) Read(io *kaitai.Stream, parent *TplinkMinifs_BodyV3, root *TplinkMinifs) (err error) {
	this._io = io
	this._parent = parent
	this._root = root

	for i := 0;; i++ {
		tmp70, err := this._io.EOF()
		if err != nil {
			return err
		}
		if tmp70 {
			break
		}
		tmp71, err := this._io.ReadBytesTerm(0, false, true, true)
		if err != nil {
			return err
		}
		this.Names = append(this.Names, string(tmp71))
	}
	return err
}