#!/bin/sh
set -eu

script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
fixture_dir=${FIXTURE_DIR:-"$script_dir/fixture"}
artifact_dir=${ARTIFACT_DIR:-"$script_dir/artifacts"}

for tool in mkfs.erofs fsck.erofs dump.erofs python3 sha256sum; do
	command -v "$tool" >/dev/null 2>&1 || {
		echo "missing tool: $tool" >&2
		exit 1
	}
done
test "$(id -u)" -eq 0 || {
	echo "root is required to create device-node fixtures" >&2
	exit 1
}
test ! -e "$fixture_dir" || {
	echo "fixture directory already exists: $fixture_dir" >&2
	exit 1
}
test ! -e "$artifact_dir" || {
	echo "artifact directory already exists: $artifact_dir" >&2
	exit 1
}

mkdir -p \
	"$fixture_dir/inline" \
	"$fixture_dir/special" \
	"$fixture_dir/namei/alpha/bravo/charlie" \
	"$fixture_dir/namei/alpha/sibling" \
	"$fixture_dir/namei/wide" \
	"$fixture_dir/pager" \
	"$fixture_dir/nfs/basic/subdir" \
	"$fixture_dir/nlink/subdir" \
	"$artifact_dir"

printf 'inline-tail-payload-0123456789\n' > "$fixture_dir/inline/inline.txt"
printf 'special target\n' > "$fixture_dir/special/target"
ln -s target "$fixture_dir/special/link"
mknod "$fixture_dir/special/char-large" c 2748 344865
mknod "$fixture_dir/special/block-large" b 2748 344865
mkfifo "$fixture_dir/special/fifo"

printf 'cold nested lookup payload\n' > \
	"$fixture_dir/namei/alpha/bravo/charlie/payload.txt"
printf 'repeat lookup payload\n' > \
	"$fixture_dir/namei/alpha/bravo/repeat.txt"
printf 'sibling marker\n' > "$fixture_dir/namei/alpha/sibling/marker.txt"
index=0
while [ "$index" -lt 320 ]; do
	name=$(printf 'entry-%03d-abcdefghijklmnopqrstuvwxyz.txt' "$index")
	printf 'wide entry %03d\n' "$index" > "$fixture_dir/namei/wide/$name"
	index=$((index + 1))
done

python3 - "$fixture_dir" <<'PY'
from pathlib import Path
import sys

root = Path(sys.argv[1])
data = bytes(((index * 131 + 17) ^ (index >> 3)) & 0xff
             for index in range(5 * 4096 + 731))
(root / "pager" / "pager.bin").write_bytes(data)
(root / "nfs" / "basic" / "regular.txt").write_text(
    "stable file handle payload\n", encoding="ascii")
(root / "nfs" / "basic" / "subdir" / "child.txt").write_text(
    "child\n", encoding="ascii")
PY
ln -s regular.txt "$fixture_dir/nfs/basic/link-to-regular"
mkfifo "$fixture_dir/nfs/basic/test.fifo"

printf 'single\n' > "$fixture_dir/nlink/single.txt"
printf 'hardlinked\n' > "$fixture_dir/nlink/hard-a.txt"
ln "$fixture_dir/nlink/hard-a.txt" "$fixture_dir/nlink/hard-b.txt"
printf 'child\n' > "$fixture_dir/nlink/subdir/child.txt"

find "$fixture_dir" -exec touch -h -t 197001010000.00 {} +

build_image()
{
	uuid=$1
	image=$2
	source=$3
	shift 3
	mkfs.erofs -d0 -x-1 -T0 --all-time --all-root --workers=1 \
		-U "$uuid" "$@" "$artifact_dir/$image" "$fixture_dir/$source"
}

build_image 11111111-2222-3333-4444-555555555551 inline.erofs inline
build_image 11111111-2222-3333-4444-555555555552 \
	special-compact.erofs special -E force-inode-compact
build_image 11111111-2222-3333-4444-555555555553 \
	special-extended.erofs special -E force-inode-extended
build_image 11111111-2222-3333-4444-555555555554 namei-base.erofs namei
build_image 11111111-2222-3333-4444-555555555555 \
	pager-plain.erofs pager -E noinline_data
build_image 11111111-2222-3333-4444-555555555556 \
	pager-lz4.erofs pager -z lz4
build_image aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee1 nfs-a.erofs nfs
build_image aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee2 nfs-b.erofs nfs
build_image 11111111-2222-3333-4444-555555555557 \
	nlink.erofs nlink -E force-inode-compact

ARTIFACT_DIR="$artifact_dir" python3 <<'PY'
from pathlib import Path
import hashlib
import os
import struct

artifact_dir = Path(os.environ["ARTIFACT_DIR"])


def u16(image, offset):
    return struct.unpack_from("<H", image, offset)[0]


def u32(image, offset):
    return struct.unpack_from("<I", image, offset)[0]


def put_u16(image, offset, value):
    struct.pack_into("<H", image, offset, value)


def put_u32(image, offset, value):
    struct.pack_into("<I", image, offset, value)


def update_superblock_checksum(image):
    polynomial = 0x82F63B78
    checksum = 0xFFFFFFFF
    put_u32(image, 1028, 0)
    for byte in image[1024:4096]:
        checksum ^= byte
        for _ in range(8):
            checksum = (checksum >> 1) ^ (
                polynomial if checksum & 1 else 0)
    put_u32(image, 1028, checksum & 0xFFFFFFFF)


def inode_offset(image, nid):
    block_bits = image[1036]
    meta_blkaddr = u32(image, 1064)
    return (meta_blkaddr << block_bits) + (nid << 5)


def compact_inode(image, nid):
    offset = inode_offset(image, nid)
    inode_format = u16(image, offset)
    assert (inode_format & 1) == 0
    return offset, (inode_format >> 1) & 7, u32(image, offset + 8)


def inline_dir_entries(image, nid):
    inode = inode_offset(image, nid)
    inode_format = u16(image, inode)
    inode_size = 64 if inode_format & 1 else 32
    layout = (inode_format >> 1) & 7
    size = (struct.unpack_from("<Q", image, inode + 8)[0]
            if inode_size == 64 else u32(image, inode + 8))
    assert layout == 2 and u16(image, inode + 2) == 0
    data = inode + inode_size
    first_nameoff = u16(image, data + 8)
    assert first_nameoff >= 12 and first_nameoff % 12 == 0
    count = first_nameoff // 12
    entries = []
    for index in range(count):
        entry = data + index * 12
        child_nid, nameoff = struct.unpack_from("<QH", image, entry)
        endoff = (u16(image, data + (index + 1) * 12 + 8)
                  if index + 1 < count else size)
        name = bytes(image[data + nameoff:data + endoff]).split(b"\0", 1)[0]
        assert name
        entries.append((child_nid, nameoff, endoff, name))
    return inode, data, size, entries


def write_image(name, image):
    update_superblock_checksum(image)
    (artifact_dir / name).write_bytes(image)


inline = bytearray((artifact_dir / "inline.erofs").read_bytes())
_, _, _, inline_root = inline_dir_entries(inline, u16(inline, 1038))
inline_nid = next(nid for nid, _, _, name in inline_root if name == b"inline.txt")
inline_inode, layout, inline_size = compact_inode(inline, inline_nid)
assert layout == 2 and u16(inline, inline_inode + 2) == 0
inline_cross_xattr_icount = 695
inline_xattr_size = 12 + 4 * (inline_cross_xattr_icount - 1)
inline_data_off = inline_inode + 32 + inline_xattr_size
block_size = 1 << inline[1036]
assert inline_data_off % block_size + inline_size > block_size
inline_cross = bytearray(inline)
put_u16(inline_cross, inline_inode + 2, inline_cross_xattr_icount)
write_image("inline-cross-block.erofs", inline_cross)

namei = bytearray((artifact_dir / "namei-base.erofs").read_bytes())
root_nid = u16(namei, 1038)
root_inode, root_data, root_size, root_entries = inline_dir_entries(
    namei, root_nid)
wide_nid = next(nid for nid, _, _, name in root_entries if name == b"wide")
wide_inode, wide_layout, _ = compact_inode(namei, wide_nid)
assert wide_layout == 2
block_bits = namei[1036]
wide_block = u32(namei, wide_inode + 16) << block_bits
wide_first_nameoff = u16(namei, wide_block + 8)
wide_count = wide_first_nameoff // 12
wide_last_nameoff = u16(namei, wide_block + (wide_count - 1) * 12 + 8)
padding_nul = namei.index(0, wide_block + wide_last_nameoff,
                          wide_block + (1 << block_bits))

nonzero_padding = bytearray(namei)
nonzero_padding[padding_nul + 1:padding_nul + 9] = b"PAD!ERO!"
write_image("namei-padding-nonzero.erofs", nonzero_padding)

short_block = bytearray(namei)
put_u32(short_block, root_inode + 8, 8)
write_image("namei-corrupt-short.erofs", short_block)

bad_nameoff = bytearray(namei)
first_nameoff = u16(bad_nameoff, root_data + 8)
put_u16(bad_nameoff, root_data + 12 + 8, first_nameoff)
write_image("namei-corrupt-nameoff.erofs", bad_nameoff)

bad_name = bytearray(namei)
slash_offset = wide_block + wide_last_nameoff + 5
assert bad_name[slash_offset] not in (0, ord("/"))
bad_name[slash_offset] = ord("/")
write_image("namei-corrupt-name.erofs", bad_name)

nlink = bytearray((artifact_dir / "nlink.erofs").read_bytes())
nlink_root_nid = u16(nlink, 1038)
_, _, _, nlink_entries = inline_dir_entries(nlink, nlink_root_nid)
single_nid = next(nid for nid, _, _, name in nlink_entries
                  if name == b"single.txt")
single_inode, _, _ = compact_inode(nlink, single_nid)
nlink1 = bytearray(nlink)
put_u16(nlink1, single_inode, u16(nlink1, single_inode) | 0x10)
put_u16(nlink1, single_inode + 6, 0x1234)
write_image("nlink1-patched.erofs", nlink1)

with (artifact_dir / "fixture-evidence.txt").open("w", encoding="ascii") as out:
    out.write(f"inline_nid={inline_nid} inode_off={inline_inode} "
              f"inode_blockoff={inline_inode % block_size} "
              f"xattr_icount=0->{inline_cross_xattr_icount} "
              f"inline_data_blockoff={inline_data_off % block_size} "
              f"inline_size={inline_size}\n")
    out.write(f"wide_nid={wide_nid} block={wide_block >> block_bits} "
              f"dirents={wide_count} last_nameoff={wide_last_nameoff} "
              f"padding_patch={padding_nul + 1 - wide_block}:"
              f"{padding_nul + 9 - wide_block}\n")
    out.write(f"nlink1_nid={single_nid} i_format_bit4=1 i_nb=0x1234\n")
    for image_name in ("special-compact.erofs", "special-extended.erofs"):
        image = bytearray((artifact_dir / image_name).read_bytes())
        special_root = u16(image, 1038)
        _, _, _, entries = inline_dir_entries(image, special_root)
        out.write(image_name + "\n")
        for nid, _, _, name in entries:
            if name not in (b"char-large", b"block-large", b"fifo"):
                continue
            offset = inode_offset(image, nid)
            out.write(f" {name.decode()} nid={nid} inode_size="
                      f"{64 if u16(image, offset) & 1 else 32} "
                      f"raw_u={u32(image, offset + 16):#010x}\n")

with (artifact_dir / "SHA256SUMS").open("w", encoding="ascii") as sums:
    for path in sorted(artifact_dir.glob("*.erofs")):
        sums.write(f"{hashlib.sha256(path.read_bytes()).hexdigest()}  "
                   f"{path.name}\n")
PY

fsck.erofs -d1 "$artifact_dir/namei-padding-nonzero.erofs"
dump.erofs --cat --path=/wide/entry-079-abcdefghijklmnopqrstuvwxyz.txt \
	"$artifact_dir/namei-padding-nonzero.erofs"
cat "$artifact_dir/fixture-evidence.txt"
cat "$artifact_dir/SHA256SUMS"
