package types

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"strings"
)

//go:generate stringer -type=j_obj_types,j_obj_kinds,dir_rec_flags,mode_t,dir_ent_file_type -output j_string.go

type j_obj_types byte // FIXME: what type

const (
	APFS_TYPE_ANY           j_obj_types = 0
	APFS_TYPE_SNAP_METADATA j_obj_types = 1
	APFS_TYPE_EXTENT        j_obj_types = 2
	APFS_TYPE_INODE         j_obj_types = 3
	APFS_TYPE_XATTR         j_obj_types = 4
	APFS_TYPE_SIBLING_LINK  j_obj_types = 5
	APFS_TYPE_DSTREAM_ID    j_obj_types = 6
	APFS_TYPE_CRYPTO_STATE  j_obj_types = 7
	APFS_TYPE_FILE_EXTENT   j_obj_types = 8
	APFS_TYPE_DIR_REC       j_obj_types = 9
	APFS_TYPE_DIR_STATS     j_obj_types = 10
	APFS_TYPE_SNAP_NAME     j_obj_types = 11
	APFS_TYPE_SIBLING_MAP   j_obj_types = 12
	APFS_TYPE_FILE_INFO     j_obj_types = 13

	APFS_TYPE_MAX_VALID j_obj_types = 13
	APFS_TYPE_MAX       j_obj_types = 15

	APFS_TYPE_INVALID j_obj_types = 15
)

type j_obj_kinds byte

const (
	APFS_KIND_ANY           j_obj_kinds = 0
	APFS_KIND_NEW           j_obj_kinds = 1
	APFS_KIND_UPDATE        j_obj_kinds = 2
	APFS_KIND_DEAD          j_obj_kinds = 3
	APFS_KIND_UPDATE_RECENT j_obj_kinds = 4

	APFS_KIND_INVALID j_obj_kinds = 255
)

type j_inode_flags uint64

const (
	INODE_IS_APFS_PRIVATE        j_inode_flags = 0x00000001
	INODE_MAINTAIN_DIR_STATS     j_inode_flags = 0x00000002
	INODE_DIR_STATS_ORIGIN       j_inode_flags = 0x00000004
	INODE_PROT_CLASS_EXPLICIT    j_inode_flags = 0x00000008
	INODE_WAS_CLONED             j_inode_flags = 0x00000010
	INODE_FLAG_UNUSED            j_inode_flags = 0x00000020
	INODE_HAS_SECURITY_EA        j_inode_flags = 0x00000040
	INODE_BEING_TRUNCATED        j_inode_flags = 0x00000080
	INODE_HAS_FINDER_INFO        j_inode_flags = 0x00000100
	INODE_IS_SPARSE              j_inode_flags = 0x00000200
	INODE_WAS_EVER_CLONED        j_inode_flags = 0x00000400
	INODE_ACTIVE_FILE_TRIMMED    j_inode_flags = 0x00000800
	INODE_PINNED_TO_MAIN         j_inode_flags = 0x00001000
	INODE_PINNED_TO_TIER2        j_inode_flags = 0x00002000
	INODE_HAS_RSRC_FORK          j_inode_flags = 0x00004000
	INODE_NO_RSRC_FORK           j_inode_flags = 0x00008000
	INODE_ALLOCATION_SPILLEDOVER j_inode_flags = 0x00010000
	INODE_FAST_PROMOTE           j_inode_flags = 0x00020000
	INODE_HAS_UNCOMPRESSED_SIZE  j_inode_flags = 0x00040000
	INODE_IS_PURGEABLE           j_inode_flags = 0x00080000
	INODE_WANTS_TO_BE_PURGEABLE  j_inode_flags = 0x00100000
	INODE_IS_SYNC_ROOT           j_inode_flags = 0x00200000
	INODE_SNAPSHOT_COW_EXEMPTION j_inode_flags = 0x00400000

	INODE_INHERITED_INTERNAL_FLAGS j_inode_flags = (INODE_MAINTAIN_DIR_STATS | INODE_SNAPSHOT_COW_EXEMPTION)

	INODE_CLONED_INTERNAL_FLAGS     j_inode_flags = (INODE_HAS_RSRC_FORK | INODE_NO_RSRC_FORK | INODE_HAS_FINDER_INFO | INODE_SNAPSHOT_COW_EXEMPTION)
	APFS_VALID_INTERNAL_INODE_FLAGS j_inode_flags = (INODE_IS_APFS_PRIVATE | INODE_MAINTAIN_DIR_STATS | INODE_DIR_STATS_ORIGIN | INODE_PROT_CLASS_EXPLICIT | INODE_WAS_CLONED | INODE_HAS_SECURITY_EA | INODE_BEING_TRUNCATED | INODE_HAS_FINDER_INFO | INODE_IS_SPARSE | INODE_WAS_EVER_CLONED | INODE_ACTIVE_FILE_TRIMMED | INODE_PINNED_TO_MAIN | INODE_PINNED_TO_TIER2 | INODE_HAS_RSRC_FORK | INODE_NO_RSRC_FORK | INODE_ALLOCATION_SPILLEDOVER | INODE_FAST_PROMOTE | INODE_HAS_UNCOMPRESSED_SIZE | INODE_IS_PURGEABLE | INODE_WANTS_TO_BE_PURGEABLE | INODE_IS_SYNC_ROOT | INODE_SNAPSHOT_COW_EXEMPTION)
	APFS_INODE_PINNED_MASK          j_inode_flags = (INODE_PINNED_TO_MAIN | INODE_PINNED_TO_TIER2)
)

func (f j_inode_flags) String() string {
	var out []string
	if f&INODE_IS_APFS_PRIVATE != 0 {
		out = append(out, "IS_APFS_PRIVATE")
	}
	if f&INODE_MAINTAIN_DIR_STATS != 0 {
		out = append(out, "MAINTAIN_DIR_STATS")
	}
	if f&INODE_DIR_STATS_ORIGIN != 0 {
		out = append(out, "DIR_STATS_ORIGIN")
	}
	if f&INODE_PROT_CLASS_EXPLICIT != 0 {
		out = append(out, "PROT_CLASS_EXPLICIT")
	}
	if f&INODE_WAS_CLONED != 0 {
		out = append(out, "WAS_CLONED")
	}
	if f&INODE_FLAG_UNUSED != 0 {
		out = append(out, "FLAG_UNUSED")
	}
	if f&INODE_HAS_SECURITY_EA != 0 {
		out = append(out, "HAS_SECURITY_EA")
	}
	if f&INODE_BEING_TRUNCATED != 0 {
		out = append(out, "BEING_TRUNCATED")
	}
	if f&INODE_HAS_FINDER_INFO != 0 {
		out = append(out, "HAS_FINDER_INFO")
	}
	if f&INODE_IS_SPARSE != 0 {
		out = append(out, "IS_SPARSE")
	}
	if f&INODE_WAS_EVER_CLONED != 0 {
		out = append(out, "WAS_EVER_CLONED")
	}
	if f&INODE_ACTIVE_FILE_TRIMMED != 0 {
		out = append(out, "ACTIVE_FILE_TRIMMED")
	}
	if f&INODE_PINNED_TO_MAIN != 0 {
		out = append(out, "PINNED_TO_MAIN")
	}
	if f&INODE_PINNED_TO_TIER2 != 0 {
		out = append(out, "PINNED_TO_TIER2")
	}
	if f&INODE_HAS_RSRC_FORK != 0 {
		out = append(out, "HAS_RSRC_FORK")
	}
	if f&INODE_NO_RSRC_FORK != 0 {
		out = append(out, "NO_RSRC_FORK")
	}
	if f&INODE_ALLOCATION_SPILLEDOVER != 0 {
		out = append(out, "ALLOCATION_SPILLEDOVER")
	}
	if f&INODE_FAST_PROMOTE != 0 {
		out = append(out, "FAST_PROMOTE")
	}
	if f&INODE_HAS_UNCOMPRESSED_SIZE != 0 {
		out = append(out, "HAS_UNCOMPRESSED_SIZE")
	}
	if f&INODE_IS_PURGEABLE != 0 {
		out = append(out, "IS_PURGEABLE")
	}
	if f&INODE_WANTS_TO_BE_PURGEABLE != 0 {
		out = append(out, "WANTS_TO_BE_PURGEABLE")
	}
	if f&INODE_IS_SYNC_ROOT != 0 {
		out = append(out, "IS_SYNC_ROOT")
	}
	if f&INODE_SNAPSHOT_COW_EXEMPTION != 0 {
		out = append(out, "SNAPSHOT_COW_EXEMPTIO")
	}
	return strings.Join(out, "|")
}

type j_xattr_flags uint16

const (
	XATTR_DATA_STREAM       j_xattr_flags = 0x00000001
	XATTR_DATA_EMBEDDED     j_xattr_flags = 0x00000002
	XATTR_FILE_SYSTEM_OWNED j_xattr_flags = 0x00000004
	XATTR_RESERVED_8        j_xattr_flags = 0x00000008
)

func (f j_xattr_flags) DataStream() bool {
	return (f & XATTR_DATA_STREAM) != 0
}
func (f j_xattr_flags) DataEmbedded() bool {
	return (f & XATTR_DATA_EMBEDDED) != 0
}
func (f j_xattr_flags) FileSystemOwned() bool {
	return (f & XATTR_FILE_SYSTEM_OWNED) != 0
}
func (f j_xattr_flags) String() string {
	var fout []string
	if f.DataStream() {
		fout = append(fout, "DATA_STREAM")
	}
	if f.DataEmbedded() {
		fout = append(fout, "DATA_EMBEDDED")
	}
	if f.FileSystemOwned() {
		fout = append(fout, "FILE_SYSTEM_OWNED")
	}
	return strings.Join(fout, "|")
}

type dir_rec_flags uint16

const (
	DREC_TYPE_MASK dir_rec_flags = 0x000f
	RESERVED_10    dir_rec_flags = 0x0010
)

const (
	/** Inode Numbers **/
	INVALID_INO_NUM       = 0
	ROOT_DIR_PARENT       = 1
	ROOT_DIR_INO_NUM      = 2
	PRIV_DIR_INO_NUM      = 3
	SNAP_DIR_INO_NUM      = 6
	PURGEABLE_DIR_INO_NUM = 7

	MIN_USER_INO_NUM = 16

	UNIFIED_ID_SPACE_MARK = 0x0800000000000000

	/** Extended Attributes Constants **/
	XATTR_MAX_EMBEDDED_SIZE                              = 3804 // = 3 Ki + 732
	XATTR_APFS_COW_EXEMPT_COUNT_NAME                     = "com.apple.fs.cow-exempt-file-count"
	XATTR_DB_REBUILD_IN_PROGRESS_EA_NAME                 = "com.apple.assetsd.dbRebuildInProgress"
	XATTR_DB_REBUILD_UUID_EA_NAME                        = "com.apple.assetsd.dbRebuildUuid"
	XATTR_THUMBNAIL_CAMERA_PREVIEW_IMAGE_ASSETID_EA_NAME = "com.apple.assetsd.thumbnailCameraPreviewImageAssetID"
	XATTR_UUID_EA_NAME                                   = "com.apple.assetsd.UUID"
	XATTR_DECMPFS_EA_NAME                                = "com.apple.decmpfs"
	XATTR_FINDER_INFO_EA_NAME                            = "com.apple.FinderInfo"
	XATTR_SYMLINK_EA_NAME                                = "com.apple.fs.symlink"
	XATTR_FIRMLINK_EA_NAME                               = "com.apple.fs.firmlink"
	XATTR_INFO_EA_NAME                                   = "com.apple.genstore.info"
	XATTR_ORIG_DISPLAY_NAME_EA_NAME                      = "com.apple.genstore.origdisplayname"
	XATTR_ORIG_PERMS_V1_EA_NAME                          = "com.apple.genstore.orig_perms_v1"
	XATTR_ORIG_POSIX_NAME_EA_NAME                        = "com.apple.genstore.origposixname"
	XATTR_SHA1_EA_NAME                                   = "com.apple.GeoServices.SHA1"
	XATTR_INSTALL_TYPE_EA_NAME                           = "com.apple.installd.installType"
	XATTR_UNIQUE_INSTALLID_EA_NAME                       = "com.apple.installd.uniqueInstallID"
	XATTR_PS_EA_NAME                                     = "com.apple.lastuseddate#PS"
	XATTR_KMD_ITEM_USER_TAGS_EA_NAME                     = "com.apple.metadata:_kMDItemUserTags"
	XATTR_COM_APPLE_BACKUP_EXCLUDEITEM_EA_NAME           = "com.apple.metadata:com_apple_backup_excludeItem"
	XATTR_KMD_ITEM_DOWNLOADED_DATE_EA_NAME               = "com.apple.metadata:kMDItemDownloadedDate"
	XATTR_KMD_ITEM_WHERE_FROMS_EA_NAME                   = "com.apple.metadata:kMDItemWhereFroms"
	XATTR_KMD_LABEL_EA_NAME                              = "com.apple.metadata:kMDLabel_fwlfb7nbt2o7degof3q2o2btjy"
	XATTR_QUARANTINE_EA_NAME                             = "com.apple.quarantine"
	XATTR_RESOURCEFORK_EA_NAME                           = "com.apple.ResourceFork"
	XATTR_ROOTLESS_EA_NAME                               = "com.apple.rootless"
	XATTR_SECURITY_EA_NAME                               = "com.apple.system.Security"
	XATTR_TEXT_ENCODING_EA_NAME                          = "com.apple.TextEncoding"
	XATTR_LAST_UPGRADE_CHECK_EA_NAME                     = "LastUpgradeCheck"
	XATTR_LOCK_EA_NAME                                   = "lock"
	XATTR_CRASHPAD_DB_INITIALIZED_EA_NAME                = "org.chromium.crashpad.database.initialized"

	/** File-System Object Constants **/
	OWNING_OBJ_ID_INVALID uint64 = 0xFFFFFFFFFFFFFFFF
	OWNING_OBJ_ID_UNKNOWN uint64 = 0xFFFFFFFFFFFFFFFE

	JOBJ_MAX_KEY_SIZE   = 832
	JOBJ_MAX_VALUE_SIZE = 3808 // = 3 Ki + 736

	MIN_DOC_ID = 3

	/** File Extent Constants **/
	FEXT_CRYPTO_ID_IS_TWEAK = 0x01
)

/** File Modes **/

/**
 * Called `mode_t` in the spec, but this clashes with the GNU `mode_t` on
 * non-Apple platforms, so we use a distinct name for portability.
 */
type mode_t uint16
type dir_ent_file_type uint16

const (
	S_IFMT mode_t = 0170000 // The bit mask used to access the file type.

	FIFO mode_t = 0010000 // A named pipe.
	CHR  mode_t = 0020000 // A character-special file.
	DIR  mode_t = 0040000 // A directory.
	BLK  mode_t = 0060000 // A block-special file.
	REG  mode_t = 0100000 // A regular file.
	LNK  mode_t = 0120000 // A symbolic link.
	SOCK mode_t = 0140000 // A socket.
	WHT  mode_t = 0160000 // A whiteout.

	/** Directory Entry File Types **/
	DT_UNKNOWN dir_ent_file_type = 0
	DT_FIFO    dir_ent_file_type = 1  // A named pipe.
	DT_CHR     dir_ent_file_type = 2  // A character-special file.
	DT_DIR     dir_ent_file_type = 4  // A directory.
	DT_BLK     dir_ent_file_type = 6  // A block-special file.
	DT_REG     dir_ent_file_type = 8  // A regular file.
	DT_LNK     dir_ent_file_type = 10 // A symbolic link.
	DT_SOCK    dir_ent_file_type = 12 // A socket.
	DT_WHT     dir_ent_file_type = 14 // A whiteout.
)

const (
	OBJ_ID_MASK    = 0x0fffffffffffffff
	OBJ_TYPE_MASK  = 0xf000000000000000
	OBJ_TYPE_SHIFT = 60

	SYSTEM_OBJ_ID_MARK = 0x0fffffff00000000
)

// JKeyT is a j_key_t
type JKeyT struct {
	ObjIDAndType uint64
} // __attribute__((packed))

func (j JKeyT) GetID() uint64 {
	return j.ObjIDAndType & OBJ_ID_MASK
}
func (j JKeyT) GetType() j_obj_types {
	return j_obj_types((j.ObjIDAndType & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT)
}

type j_inode_key_t struct {
	Hdr JKeyT
} // __attribute__((packed))

type bsd_flags_t uint32

const (
	/*
	 * Definitions of flags stored in file flags word.
	 *
	 * Super-user and owner changeable flags.
	 */
	UF_SETTABLE bsd_flags_t = 0x0000ffff /* mask of owner changeable flags */
	NONE        bsd_flags_t = 0x00000000
	NODUMP      bsd_flags_t = 0x00000001 /* do not dump file */
	IMMUTABLE   bsd_flags_t = 0x00000002 /* file may not be changed */
	APPEND      bsd_flags_t = 0x00000004 /* writes to file may only append */
	OPAQUE      bsd_flags_t = 0x00000008 /* directory is opaque wrt. union */
	/*
	 * The following bit is reserved for FreeBSD.  It is not implemented
	 * in Mac OS X.
	 */
	NOUNLINK   bsd_flags_t = 0x00000010 /* file may not be removed or renamed */
	COMPRESSED bsd_flags_t = 0x00000020 /* file is compressed (some file-systems) */

	/* UF_TRACKED is used for dealing with document IDs.  We no longer issue
	 *  notifications for deletes or renames for files which have UF_TRACKED set. */
	TRACKED bsd_flags_t = 0x00000040

	DATAVAULT bsd_flags_t = 0x00000080 /* entitlement required for reading and writing */

	/* Bits 0x0100 through 0x4000 are currently undefined. */
	HIDDEN bsd_flags_t = 0x00008000 /* hint that this item should not be displayed in a GUI */
	/*
	 * Super-user changeable flags.
	 */
	SF_SUPPORTED  bsd_flags_t = 0x009f0000 /* mask of superuser supported flags */
	SF_SETTABLE   bsd_flags_t = 0x3fff0000 /* mask of superuser changeable flags */
	SF_SYNTHETIC  bsd_flags_t = 0xc0000000 /* mask of system read-only synthetic flags */
	SF_ARCHIVED   bsd_flags_t = 0x00010000 /* file is archived */
	SF_IMMUTABLE  bsd_flags_t = 0x00020000 /* file may not be changed */
	SF_APPEND     bsd_flags_t = 0x00040000 /* writes to file may only append */
	SF_RESTRICTED bsd_flags_t = 0x00080000 /* entitlement required for writing */
	SF_NOUNLINK   bsd_flags_t = 0x00100000 /* Item may not be removed, renamed or mounted on */
	/*
	 * The following two bits are reserved for FreeBSD.  They are not
	 * implemented in Mac OS X.
	 */
	/* NOTE: There is no SF_HIDDEN bit. */
	SF_SNAPSHOT bsd_flags_t = 0x00200000 /* snapshot inode */
	SF_FIRMLINK bsd_flags_t = 0x00800000 /* file is a firmlink */
	/*
	 * Synthetic flags.
	 *
	 * These are read-only.  We keep them out of SF_SUPPORTED so that
	 * attempts to set them will fail.
	 */
	SF_DATALESS bsd_flags_t = 0x40000000 /* file is dataless object */
)

func (f bsd_flags_t) String() string {
	var out []string
	if f == NONE {
		out = append(out, "NONE")
	}
	if f&NODUMP != 0 {
		out = append(out, "UF_NODUMP")
	}
	if f&IMMUTABLE != 0 {
		out = append(out, "UF_IMMUTABLE")
	}
	if f&APPEND != 0 {
		out = append(out, "UF_APPEND")
	}
	if f&OPAQUE != 0 {
		out = append(out, "UF_OPAQUE")
	}
	if f&NOUNLINK != 0 {
		out = append(out, "UF_NOUNLINK")
	}
	if f&COMPRESSED != 0 {
		out = append(out, "UF_COMPRESSED")
	}
	if f&TRACKED != 0 {
		out = append(out, "UF_TRACKED")
	}
	if f&DATAVAULT != 0 {
		out = append(out, "UF_DATAVAULT")
	}
	if f&HIDDEN != 0 {
		out = append(out, "UF_HIDDEN")
	}
	if f&SF_ARCHIVED != 0 {
		out = append(out, "SF_ARCHIVED")
	}
	if f&SF_IMMUTABLE != 0 {
		out = append(out, "SF_IMMUTABLE")
	}
	if f&SF_APPEND != 0 {
		out = append(out, "SF_APPEND")
	}
	if f&SF_RESTRICTED != 0 {
		out = append(out, "SF_RESTRICTED")
	}
	if f&SF_NOUNLINK != 0 {
		out = append(out, "SF_NOUNLINK")
	}
	if f&SF_SNAPSHOT != 0 {
		out = append(out, "SF_SNAPSHOT")
	}
	if f&SF_FIRMLINK != 0 {
		out = append(out, "SF_FIRMLINK")
	}
	if f&SF_DATALESS != 0 {
		out = append(out, "SF_DATALESS")
	}
	return strings.Join(out, "|")
}

type j_inode_val_t struct {
	ParentID               uint64
	PrivateID              uint64
	CreateTime             EpochTime
	ModTime                EpochTime
	ChangeTime             EpochTime
	AccessTime             EpochTime
	InternalFlags          j_inode_flags
	NchildrenOrNlink       int32 // union
	DefaultProtectionClass cp_key_class_t
	WriteGenerationCounter uint32
	BsdFlags               bsd_flags_t
	Owner                  uid_t
	Group                  gid_t
	Mode                   mode_t
	Pad1                   uint16
	UncompressedSize       uint64 // formerly `pad2`
	// Xfields                []uint8
} // __attribute__((packed))

type JInodeVal struct {
	j_inode_val_t
	blob    xf_blob
	Xfields []Xfield
}

func (v JInodeVal) String() string {
	vout := fmt.Sprintf("parent_id=%#x, private_id=%#x, create_time=%s, mod_time=%s, change_time=%s, access_time=%s, flags=%s, nchildren_or_nlink=%d, default_protection_class=%s, write_gen_cnt=%d, bsd_flags=%s, owner=%d, group=%d, mode=%s, uncompressed_size=%d",
		v.ParentID,
		v.PrivateID,
		v.CreateTime,
		v.ModTime,
		v.ChangeTime,
		v.AccessTime,
		v.InternalFlags,
		v.NchildrenOrNlink,
		v.DefaultProtectionClass,
		v.WriteGenerationCounter,
		v.BsdFlags,
		v.Owner,
		v.Group,
		v.Mode&S_IFMT,
		v.UncompressedSize,
	)
	if len(v.Xfields) > 0 {
		var xfout []string
		for _, xf := range v.Xfields {
			xfout = append(xfout, fmt.Sprintf("%s: %s", xf.XType, xf))
		}
		vout += fmt.Sprintf(", xfields={%s}", strings.Join(xfout, " ,"))
	}
	return vout
}

type j_drec_key_t struct {
	// Hdr     JKeyT
	NameLen uint16 // NOTE: Not `name_len_and_hash` as the spec erroneously says.
	Name    string
} // __attribute__((packed))

/**
 * NOTE: The spec says that if a file-system record is of type
 * `APFS_TYPE_DIR_REC`, then the record's key is an instance of `j_drec_key_t`.
 * However, the type `j_drec_hashed_key_t` (seen below) is defined in the spec
 * but not used anywhere in the spec; and upon closer inspection, the keys I
 * have encountered in practice exclusively appear to be instances of
 * `j_drec_hashed_key_t`.
 *
 * As such, either:
 * (a) `j_drec_key_t` has been silently deprecated as of 2019-10-31 and replaced
 *      by `j_drec_hashed_key_t`; or
 * (b) the specific type (`j_drec_key_t` vs. `j_drec_hashed_key_t`) must be
 *      determined by some convoluted means (i.e. case analysis of the data
 *      contained in the key).
 *
 * We assume that (a) is true, i.e. we exclusively use `j_drec_hashed_key_t`.
 */

type JDrecHashedKeyT struct {
	// Hdr            JKeyT
	NameLenAndHash uint32
	Name           string
} // __attribute__((packed))

const (
	J_DREC_LEN_MASK   uint32 = 0x000003ff
	J_DREC_HASH_MASK  uint32 = 0xfffffc00 // Spec incorrectly says `0xfffff400`
	J_DREC_HASH_SHIFT        = 10
)

func (k JDrecHashedKeyT) Length() uint32 {
	return k.NameLenAndHash & J_DREC_LEN_MASK
}

func (k JDrecHashedKeyT) Hash() uint32 {
	return (k.NameLenAndHash & J_DREC_HASH_MASK) >> J_DREC_HASH_SHIFT
}

type j_drec_val_t struct {
	FileID    uint64
	DateAdded EpochTime
	Flags     dir_ent_file_type
	// Xfields   []byte
} // __attribute__((packed))

type JDrecVal struct {
	j_drec_val_t
	blob    xf_blob
	Xfields []Xfield
}

func (v JDrecVal) String() string {
	vout := fmt.Sprintf("file_id=%#x flags=%s, date_added=%s",
		v.FileID,
		v.Flags.String(),
		v.DateAdded,
	)
	if len(v.Xfields) > 0 {
		var xfout []string
		for _, xf := range v.Xfields {
			xfout = append(xfout, fmt.Sprintf("%s: %s", xf.XType, xf))
		}
		vout += fmt.Sprintf(", xfields={%s}", strings.Join(xfout, " ,"))
	}
	return vout
}

type j_dir_stats_key_t struct {
	Hdr JKeyT
} // __attribute__((packed))

type j_dir_stats_val_t struct {
	NumChildren uint64
	TotalSize   uint64
	ChainedKey  uint64
	GenCount    uint64
} // __attribute__((packed))

func (val j_dir_stats_val_t) String() string {
	return fmt.Sprintf("num_children=%d, total_size=%d, chained_key=%#x, gen_count=%d",
		val.NumChildren,
		val.TotalSize,
		val.ChainedKey,
		val.GenCount)
}

type JXattrKeyT struct {
	// Hdr     JKeyT
	NameLen uint16
	Name    string
} // __attribute__((packed))

type JXattrValT struct {
	Flags   j_xattr_flags // The extended attribute recordʼs flags.
	DataLen uint16        // The length of the extended attribute data.
	Data    interface{}   // The extended attribute data or the identifier of a data stream that contains the data.
} // __attribute__((packed))

func (val JXattrValT) String() string {
	var vout []string
	vout = append(vout, fmt.Sprintf("flags=%s", val.Flags.String()))
	if val.Flags.DataEmbedded() {
		vout = append(vout, fmt.Sprintf("data_len=%#x", val.DataLen))
		if val.DataLen == 0x10 {
			var hdr DecmpfsDiskHeader
			binary.Read(bytes.NewReader(val.Data.([]byte)), binary.LittleEndian, &hdr.decmpfsDiskHeader)
			vout = append(vout, fmt.Sprintf("data={%s}\n%s", hdr, hexdumpColor(hex.Dump(val.Data.([]byte)))))
		} else {
			// FIXME: don't string print data
			vout = append(vout, fmt.Sprintf("data=\n%s", hexdumpColor(hex.Dump(val.Data.([]byte)))))
		}
	} else if val.Flags.DataStream() {
		vout = append(vout, val.Data.(JXattrDstreamT).String())
	} else {
		vout = append(vout, fmt.Sprintf("data=\n%s", hexdumpColor(hex.Dump(val.Data.([]byte)))))
	}

	return strings.Join(vout, ", ")
}
