commit d19025bbc9f33010d7d0e5234b2ed3098d232644 Author: Lephe Date: Thu Mar 21 22:54:06 2019 +0100 fxsdk: initial push for gint v2 (WIP but mostly done) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79b3941 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Configuration file +Makefile.cfg + +# Build directory +build/ + +# Binaries +bin/ + +# Test icons +icons/ + +# Documentation drafts +doc/ + +# Python cache +__pycache__ diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..b0a6bcb --- /dev/null +++ b/Makefile @@ -0,0 +1,119 @@ +#! /usr/bin/make -f + +# Require config file if not cleaning up +ifeq "$(filter clean distclean,$(MAKECMDGOALS))" "" +include Makefile.cfg +endif + +# Compiler flags +cflags = -Wall -Wextra -std=c11 -O2 -I $(dir $<) -D_GNU_SOURCE \ + -DFXSDK_PREFIX='"$(PREFIX)"' $(CFLAGS) +# Linker flags +lflags = -lpng +# Bison generation flags +# bflags = -L C --defines=$(@:.c=.h) --verbose +# Dependency generation flags +dflags = -MT $@ -MMD -MP -MF $(@:%.o=%.d) + +# +# Main targets and symbolic targets +# $TARGETS is provided by Makefile.cfg. +# + +TARGETS := $(filter-out fxconv,$(TARGETS)) +bin = $(TARGETS:%=bin/%) + +# fxconv has no sources files because it's written in Python +src = $(wildcard $1/*.c) +src-fxsdk := $(call src,fxsdk) +src-fxg1a := $(call src,fxg1a) +src-fxos := $(call src,fxos) + +obj = $(src-$1:%=build/%.o) +obj-fxsdk := $(call obj,fxsdk) +obj-fxg1a := $(call obj,fxg1a) +obj-fxos := $(call obj,fxos) + +# Symbolic targets + +all: $(bin) + +all-fxsdk: bin/fxsdk +all-fxg1a: bin/fxg1a +all-fxos: bin/fxos + +# Explicit targets + +bin/fxsdk: $(obj-fxsdk) | bin/ + gcc $^ -o $@ $(lflags) +bin/fxg1a: $(obj-fxg1a) | bin/ + gcc $^ -o $@ $(lflags) +bin/fxos: $(obj-fxos) | bin/ + gcc $^ -o $@ $(lflags) + +bin/: + mkdir -p $@ + +# +# Source rules +# + +build/%.c.o: %.c + @mkdir -p $(dir $@) + gcc -c $< -o $@ $(cflags) $(dflags) + +# Flex lexers (unused since fxconv is written in Python) +# build/%/lexer.yy.c: %/lexer.l build/%/parser.tab.c +# flex -o $@ -s $< +# build/%/lexer.yy.c.o: build/%/lexer.yy.c +# gcc -c $< -o $@ $(cflags) -Wno-unused-function $(dflags) -I $* + +# Bison parsers (unused since fxconv is written in Python) +# build/%/parser.tab.c: %/parser.y +# bison $< -o $@ $(bflags) +# build/%/parser.tab.c.o: build/%/parser.tab.c +# gcc -c $< -o $@ $(cflags) $(dflags) -I $* + +# +# Dependency system, misc. +# + +include $(wildcard build/*/*.d) + +# Dependency on configuration file +Makefile.cfg: + @ if [[ ! -f Makefile.cfg ]]; then \ + echo "error: Makefile.cfg is missing, did you ./configure?" >&2; \ + false; \ + fi + +.PHONY: all clean distclean + +# +# Installing +# + +install: $(bin) + install -d $(PREFIX)/bin + install $(bin) -m 755 $(PREFIX)/bin + install fxconv/fxconv-main.py -m 755 $(PREFIX)/bin/fxconv + install fxconv/fxconv.py -m 644 $(PREFIX)/bin + +# +# Cleaning +# + +clean-fxsdk: + @rm -rf build/fxsdk +clean-fxconv: + @rm -rf build/fxconv +clean-fxg1a: + @rm -rf build/fxg1a +clean-fxos: + @rm -rf build/fxos + +clean: + @rm -rf build +distclean: clean + @rm -rf bin + @rm -f Makefile.cfg diff --git a/configure b/configure new file mode 100755 index 0000000..8c01f87 --- /dev/null +++ b/configure @@ -0,0 +1,112 @@ +#! /usr/bin/bash + +# +# Output variables +# + +# Path parameters +PREFIX="/usr" +# Individual component selection +BUILD_fxsdk=1 +BUILD_fxconv=1 +BUILD_fxg1a=1 +BUILD_fxos=1 + +# +# Tool name checking +# + +check() +{ + [[ $1 = "fxsdk" ]] || + [[ $1 = "fxconv" ]] || + [[ $1 = "fxg1a" ]] || + [[ $1 = "fxos" ]] +} + +# +# Usage +# + +help() +{ + cat << EOF +Configuration options for the fxSDK (fx9860g and fxcg50 development tools). + +Tool selection: + may be one of the following: + "fxsdk" Command-line options (you generally want this) + "fxconv" Asset conversion for gint (or any 4-aligned-VRAM system) + "fxg1a" G1A file wrapper, editor and analyzer + "fxos" OS fiddling tool, including syscall disassembly + + --enable- Build and install the selected tool [default] + --disable- Do not build or install the selected tool + +Install folders: + Executables will be installed in /bin and runtime data in + /share/fxsdk. + + --prefix= Base install folder [default /usr] +EOF + exit 0 +} + +# +# Argument parsing +# + +for arg; do case "$arg" in + -h | -? | --help) + help;; + + --prefix=*) + PREFIX=${arg#--prefix=};; + + --enable-*) + tool="${arg#--enable-}" + if ! check $tool; then + echo "error: cannot enable $tool: unknown tool" + exit 1 + fi + + eval "BUILD_${tool}=1";; + + --disable-*) + tool="${arg#--disable-}" + if ! check $tool; then + echo "error: cannot disable $tool: unknown tool" + exit 1 + fi + + eval "BUILD_${tool}=0";; + + *) + echo "error: unrecognized option $arg" + exit 1;; +esac; done + +# +# Makefile generation +# + +gen() +{ + echo "PREFIX = $PREFIX" + echo -n "TARGETS =" + + [[ $BUILD_fxsdk = 1 ]] && echo -n " fxsdk" + [[ $BUILD_fxconv = 1 ]] && echo -n " fxconv" + [[ $BUILD_fxg1a = 1 ]] && echo -n " fxg1a" + [[ $BUILD_fxos = 1 ]] && echo -n " fxos" + + echo "" +} + +echo "Configuration complete, the following has been saved in Makefile.cfg:" +echo "" + +gen | tee Makefile.cfg + +echo "" +echo "You can now 'make'." diff --git a/fxconv/fxconv-main.py b/fxconv/fxconv-main.py new file mode 100755 index 0000000..a665163 --- /dev/null +++ b/fxconv/fxconv-main.py @@ -0,0 +1,112 @@ +#! /usr/bin/python3 + +import getopt +import sys +import os +import fxconv + +help_string = """ +usage: fxconv [-s] [files...] + fxconv -b -o [parameters...] + fxconv -i -o [parameters...] + fxconv -f -o [parameters...] + +fxconv converts data files such as images and fonts into gint formats +optimized for fast execution, or into object files. + +Operating modes: + -s, --script Expose the fxconv module and run this Python script + -b, --binary Turn data into an object file, no conversion + -i, --image Convert to gint's image format + -f, --font Convert to gint's font format + +When using -s, additional arguments are stored in the [fxconv.args] variable of +the module. This is intended to be a restricted list of file names specified by +a Makefile, used to convert only a subset of the files in the script. + +The -b, -i and -f modes are shortcuts to convert single files without a script. +They accept parameters with a "category.key:value" syntax, for example: + fxconv -f myfont.png -o myfont.o charset:ascii grid.padding:1 height:7 +""".strip() + +# Simple error-warnings system +def err(msg): + print("error:", msg, file=sys.stderr) +def warn(msg): + print("warning:", msg, file=sys.stderr) + +def main(): + # Default execution mode is to run a Python script for conversion + modes = "script binary image font" + mode = "s" + output = None + + # Parse command-line arguments + + if len(sys.argv) == 1: + print(help_string, file=sys.stderr) + sys.exit(1) + + try: + opts, args = getopt.gnu_getopt(sys.argv[1:], "hsbifo:", + ("help output="+modes).split()) + except getopt.GetoptError as error: + err(error) + sys.exit(1) + + for name, value in opts: + # Print usage + if name == "--help": + err(help_string, file=sys.stderr) + sys.exit(0) + # TODO: fxconv: verbose mode + elif name == "--verbose": + pass + elif name in [ "-o", "--output" ]: + output = value + # Other names are modes + else: + mode = name[1] if len(name)==2 else name[2] + + # Remaining arguments + if args == []: + err(f"execution mode -{mode} expects an input file") + sys.exit(1) + input = args.pop(0) + + # In --script mode, run the Python script with an augmented PYTHONPATH + + if mode == "s": + if output is not None: + warn("option --output is ignored in script mode") + args = None if args == [] else args + + err("script mode not currently implemented (TODO) x_x") + sys.exit(1) + + # In shortcut conversion modes, read parameters from the command-line + + else: + def check(arg): + if ':' not in arg: + warn(f"argument {arg} is not a valid parameter (ignored)") + return ':' in arg + + def insert(params, path, value): + if len(path) == 1: + params[path[0]] = value + return + if not path[0] in params: + params[path[0]] = {} + insert(params[path[0]], path[1:], value) + + args = [ arg.split(':', 1) for arg in args if check(arg) ] + params = {} + for (name, value) in args: + insert(params, name.split("."), value) + + params["type"] = { "b": "binary", "i": "image", "f": "font" }[mode] + + fxconv.convert(input, params, output) + +main() diff --git a/fxconv/fxconv.py b/fxconv/fxconv.py new file mode 100644 index 0000000..a91563e --- /dev/null +++ b/fxconv/fxconv.py @@ -0,0 +1,497 @@ +""" +fxconv: Convert data files into gint formats or object files +""" + +import os +import tempfile +import subprocess + +from PIL import Image + +# +# Color quantification +# + +# Colors +FX_BLACK = ( 0, 0, 0, 255) +FX_DARK = ( 85, 85, 85, 255) +FX_LIGHT = (170, 170, 170, 255) +FX_WHITE = (255, 255, 255, 255) +FX_ALPHA = ( 0, 0, 0, 0) + +# Profiles + +FX_PROFILES = [ + { # Usual black-and-white bitmaps without transparency, as in MonochromeLib + "name": "mono", + "gray": False, + "colors": { FX_BLACK, FX_WHITE }, + "layers": [ lambda c: (c == FX_BLACK) ] + }, + { # Black-and-white with transparency, equivalent of two bitmaps in ML + "name": "mono_alpha", + "gray": False, + "colors": { FX_BLACK, FX_WHITE, FX_ALPHA }, + "layers": [ lambda c: (c != FX_ALPHA), + lambda c: (c == FX_BLACK) ] + }, + { # Gray engine bitmaps, reference could have been Eiyeron's Gray Lib + "name": "gray", + "gray": True, + "colors": { FX_BLACK, FX_DARK, FX_LIGHT, FX_WHITE }, + "layers": [ lambda c: (c in [FX_BLACK, FX_LIGHT]), + lambda c: (c in [FX_BLACK, FX_DARK]) ] + }, + { # Gray images with transparency, unfortunately 3 layers since 5 colors + "name": "gray_alpha", + "gray": True, + "colors": { FX_BLACK, FX_DARK, FX_LIGHT, FX_WHITE, FX_ALPHA }, + "layers": [ lambda c: (c != FX_ALPHA), + lambda c: (c in [FX_BLACK, FX_LIGHT]), + lambda c: (c in [FX_BLACK, FX_DARK]) ] + }, +] + +# +# Character sets +# + +class _Charset: + def __init__(self, id, name, count): + self.id = id + self.name = name + self.count = count + +FX_CHARSETS = [ + # Digits 0...9 + _Charset(0x0, "numeric", 10), + # Uppercase letters A...Z + _Charset(0x1, "upper", 26), + # Upper and lowercase letters A..Z, a..z + _Charset(0x2, "alpha", 52), + # Letters and digits A..Z, a..z, 0..9 + _Charset(0x3, "alnum", 62), + # All printable characters from 0x20 to 0x7e + _Charset(0x4, "print", 95), + # All 128 ASII characters + _Charset(0x5, "ascii", 128), +] + +# +# Internal routines +# + +# normalize_area(): Expand area.size and set defaults for all values. +def _normalize_area(area, img): + default = { "x": 0, "y": 0, "width": img.width, "height": img.height } + if area is None: + area = default + else: + if "size" in area: + area["width"], area["height"] = area["size"].split("x") + area = { **default, **area } + + return (int(area[key]) for key in "x y width height".split()) + +class _Grid: + # [grid] is a dictionary of parameters. Relevant keys: + # "border", "padding", "width", "height", "size" + def __init__(self, grid): + self.border = int(grid.get("border", 1)) + self.padding = int(grid.get("padding", 0)) + + self.w = int(grid.get("width", "-1")) + self.h = int(grid.get("height", "-1")) + + if "size" in grid: + self.w, self.h = map(int, grid["size"].split("x")) + + if self.w <= 0 or self.h <= 0: + raise FxconvError("size of grid unspecified or invalid") + + # size(): Number of elements in the grid + def size(self, img): + b, p, w, h = self.border, self.padding, self.w, self.h + + # Padding-extended parameters + W = w + 2 * p + H = h + 2 * p + + columns = (img.width - b) // (W + b) + rows = (img.height - b) // (H + b) + return columns * rows + + + # iter(): Iterator on all rectangles of the grid + def iter(self, img): + b, p, w, h = self.border, self.padding, self.w, self.h + + # Padding-extended parameters + W = w + 2 * p + H = h + 2 * p + + columns = (img.width - b) // (W + b) + rows = (img.height - b) // (H + b) + + for r in range(rows): + for c in range(columns): + x = b + c * (W + b) + p + y = b + r * (H + b) + p + yield (x, y, x + w, y + h) + +# +# Binary conversion +# + +def _convert_binary(input, output, params): + raise FxconvError("TODO: binary mode x_x") + +# +# Image conversion +# + +def _profile_find(name): + gen = ((i,pr) for (i,pr) in enumerate(FX_PROFILES) if pr["name"] == name) + return next(gen, (None,None)) + +def _convert_image(input, output, params): + img = Image.open(input) + if img.width >= 4096 or img.height >= 4096: + raise FxconvError(f"'{input}' is too large (max. 4095*4095)") + + # Expand area.size and get the defaults. Crop image to resulting area. + params["area"] = _normalize_area(params.get("area", None), img) + img = img.crop(params["area"]) + + # Quantize the image and check the profile + img = quantize(img, dither=False) + + # If profile is provided, check its validity, otherwise use the smallest + # compatible profile + + colors = { y for (x,y) in img.getcolors() } + + if "profile" in params: + p = params["profile"] + pid, p = _profile_find(p) + if p is None: + raise FxconvError(f"unknown profile {p} in conversion '{input}'") + if colors - profiles[p]: + raise FxconvError(f"'{input}' has more colors than profile '{p}'") + else: + p = "gray" if FX_LIGHT in colors or FX_DARK in colors else "mono" + if FX_ALPHA in colors: p += "_alpha" + pid, p = _profile_find(p) + + # Make the image header + + header = bytes ([(0x80 if p["gray"] else 0) + pid]) + encode24bit = lambda x: bytes([ x >> 16, (x & 0xff00) >> 8, x & 0xff ]) + header += encode24bit((img.size[0] << 12) + img.size[1]) + + # Split the image into layers depending on the profile and zip them all + + layers = [ _image_project(img, layer) for layer in p["layers"] ] + count = len(layers) + size = len(layers[0]) + + data = bytearray(count * size) + n = 0 + + for longword in range(size // 4): + for layer in layers: + for i in range(4): + data[n] = layer[4 * longword + i] + n += 1 + + # Generate the object file + + elf(header + data, output, "_" + params["name"]) + +def _image_project(img, f): + # New width and height + w = (img.size[0] + 31) // 32 + h = (img.size[1]) + + data = bytearray(4 * w * h) + im = img.load() + + # Now generate a 32-bit byte sequence + for y in range(img.size[1]): + for x in range(img.size[0]): + bit = int(f(im[x, y])) + data[4 * y * w + (x >> 3)] |= (bit << (~x & 7)) + + return data + +# +# Font conversion +# + +def _charset_find(name): + gen = (cs for cs in FX_CHARSETS if cs.name == name) + return next(gen, None) + +def _convert_font(input, output, params): + + #-- + # Image area and grid + #-- + + img = Image.open(input) + params["area"] = _normalize_area(params.get("area", None), img) + img = img.crop(params["area"]) + + grid = _Grid(params.get("grid", {})) + + # Quantize image (any profile will do) + img = quantize(img, dither=False) + + #-- + # Character set + #-- + + if "charset" not in params: + raise FxconvError("'charset' attribute is required and missing") + + charset = _charset_find(params["charset"]) + if charset is None: + raise FxconvError(f"unknown character set '{charset}'") + if charset.count > grid.size(img): + raise FxconvError(f"not enough elements in grid (got {grid.size()}, "+ + f"need {charset.count} for '{charset.name}'") + + #-- + # Proportionality and metadata + #-- + + proportional = (params.get("proportional", "false") == "true") + + title = params.get("title", "") + if len(title) > 31: + raise FxconvError(f"font title {title} is too long (max. 31 bytes)") + # Pad title to 4 bytes + title = bytes(title, "utf-8") + bytes(((4 - len(title) % 4) % 4) * [0]) + + flags = set(params.get("flags", "").split(",")) + flags.remove("") + flags_std = { "bold", "italic", "serif", "mono" } + + if flags - flags_std: + raise FxconvError(f"unknown flags: {', '.join(flags - flags_std)}") + + bold = int("bold" in flags) + italic = int("italic" in flags) + serif = int("serif" in flags) + mono = int("mono" in flags) + header = bytes([ + (len(title) << 3) | (bold << 2) | (italic << 1) | serif, + (mono << 7) | (int(proportional) << 6) | (charset.id & 0xf), + params.get("height", grid.h), + grid.h, + ]) + + encode16bit = lambda x: bytes([ x >> 8, x & 255 ]) + fixed_header = encode16bit(grid.w) + encode16bit((grid.w*grid.h + 31) >> 5) + + #-- + # Encoding glyphs + #-- + + data_glyphs = [] + data_widths = bytearray() + data_index = bytearray() + + for (number, region) in enumerate(grid.iter(img)): + # Upate index + if not (number % 8): + idx = len(data_glyphs) // 4 + data_index += encode16bit(idx) + + # Get glyph area + glyph = img.crop(region) + glyph.save(f"/tmp/img{number}.png") + if proportional: + glyph = _trim(glyph) + data_widths.append(glyph.width) + + length = 4 * ((glyph.width * glyph.height + 31) >> 5) + bits = bytearray(length) + offset = 0 + px = glyph.load() + + for y in range(glyph.size[1]): + for x in range(glyph.size[0]): + color = (px[x,y] == FX_BLACK) + bits[offset >> 3] |= ((color * 0x80) >> (offset & 7)) + offset += 1 + + data_glyphs.append(bits) + + data_glyphs = b''.join(data_glyphs) + + #--- + # Object file generation + #--- + + if proportional: + data = header + data_index + data_widths + data_glyphs + title + else: + data = header + fixed_header + data_glyphs + title + + elf(data, output, "_" + params["name"]) + +# +# Exceptions +# + +FxconvError = Exception + +# +# API +# + +def quantize(img, dither=False): + """ + Convert a PIL.Image.Image into an RGBA image whose only colors are: + * FX_BLACK = ( 0, 0, 0, 255) + * FX_DARK = ( 85, 85, 85, 255) + * FX_LIGHT = (170, 170, 170, 255) + * FX_WHITE = (255, 255, 255, 255) + * FX_ALPHA = ( 0, 0, 0, 0) + + The alpha channel is first flattened to either opaque of full transparent, + then all colors are quantized into the 4-shade scale. Floyd-Steinberg + dithering can be used, although most applications will prefer nearest- + neighbor coloring. + + Arguments: + img -- Input image, in any format + dither -- Enable Floyd-Steinberg dithering [default: False] + + Returns a quantized PIL.Image.Image. + """ + + # Our palette will have only 4 colors for the gray engine + colors = [ FX_BLACK, FX_DARK, FX_LIGHT, FX_WHITE ] + + # Create the palette + palette = Image.new("RGBA", (len(colors), 1)) + for (i, c) in enumerate(colors): + palette.putpixel((i, 0), c) + palette = palette.convert("P") + palette.save("/tmp/palette.png") + + # Save the alpha channel, and make it either full transparent or opaque + try: + alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE) + except: + alpha_channel = Image.new("L", img.size, 255) + + # Apply the palette to the original image (transparency removed) + img = img.convert("RGB") + + # Let's do an equivalent of the following, but with a dithering setting: + # img = img.quantize(palette=palette) + + img.load() + palette.load() + im = img.im.convert("P", int(dither), palette.im) + img = img._new(im) + + # Put back the alpha channel + img.putalpha(alpha_channel) + + # Premultiply alpha + pixels = img.load() + for y in range(img.size[1]): + for x in range(img.size[0]): + r, g, b, a = pixels[x, y] + if a == 0: + r, g, b, = 0, 0, 0 + pixels[x, y] = (r, g, b, a) + + return img + +def convert(input, params, output=None): + """ + Convert a data file into an object that exports the following symbols: + * _ + * __end + * __size + The variable name is obtained from the parameter dictionary . + + Arguments: + input -- Input file path + params -- Parameter dictionary + output -- Output file name [default: with suffix '.o'] + + Produces an output file and returns nothing. + """ + + if output is None: + output = os.path.splitext(input)[0] + '.o' + + if "name" not in params: + raise FxconvError(f"no name specified for conversion '{input}'") + + if "type" not in params: + raise FxconvError(f"missing type in conversion '{input}'") + elif params["type"] == "binary": + _convert_binary(input, output, params) + elif params["type"] == "image": + _convert_image(input, output, params) + elif params["type"] == "font": + _convert_font(input, output, params) + +def elf(data, output, symbol, section=None, arch="sh3"): + """ + Call objcopy to create an object file from the specified data. The object + file will export three symbols: + * + * _end + * _size + + The symbol name must have a leading underscore if it is to be declared and + used from a C program. + + The section name can be specified, along with its flags. A typical example + would be section=".rodata,contents,alloc,load,readonly,data", which is the + default. + + The architecture can be either "sh3" or "sh4". This affects the choice of + the toolchain (sh3eb-elf-objcopy versus sh4eb-nofpu-elf-objcopy) and the + --binary-architecture flag of objcopy. + + Arguments: + data -- A bytes-like object with data to embed into the object file + output -- Name of output file + symbol -- Chosen symbol name + section -- Target section [default: above variation of .rodata] + arch -- Target architecture: "sh3" or "sh4" [default: "sh3"] + + Produces an output file and returns nothing. + """ + + toolchain = { "sh3": "sh3eb-elf", "sh4": "sh4eb-nofpu-elf" }[arch] + if section is None: + section = ".rodata,contents,alloc,load,readonly,data" + + with tempfile.NamedTemporaryFile() as fp: + fp.write(data) + fp.flush() + + sybl = "_binary_" + fp.name.replace("/", "_") + + objcopy_args = [ + f"{toolchain}-objcopy", "-I", "binary", "-O", "elf32-sh", + "--binary-architecture", arch, "--file-alignment", "4", + "--rename-section", f".data={section}", + "--redefine-sym", f"{sybl}_start={symbol}", + "--redefine-sym", f"{sybl}_end={symbol}_end", + "--redefine-sym", f"{sybl}_size={symbol}_size", + fp.name, output ] + + proc = subprocess.run(objcopy_args) + if proc.returncode != 0: + raise FxconvError(f"objcopy returned {proc.returncode}") diff --git a/fxg1a/dump.c b/fxg1a/dump.c new file mode 100644 index 0000000..cb6ed4c --- /dev/null +++ b/fxg1a/dump.c @@ -0,0 +1,146 @@ +#include +#include +#include + +#include +#include + +/* check(): Check validity of a g1a control or fixed field + + This function checks a single field of a g1a header (depending on the value + of @test, from 0 up) and returns: + * 0 if the field is valid + * 1 if there is a minor error (wrong fixed-byte entry) + * 2 if there is a major error (like not a g1a, bad checksum, etc) + * -1 if the value of @test is out of bounds + + It produces a description of the check in @status (even if the test is + passed); the string should have room for at least 81 bytes. + + @test Test number + @g1a G1A file being manipulated + @size File size + @status Array row, at least 81 bytes free */ +static int check(int test, struct g1a const *g1a, size_t size, char *status) +{ + #define m(msg, ...) sprintf(status, msg, ##__VA_ARGS__) + + struct header const *h = &g1a->header; + uint8_t const *raw = (void *)h; + + uint16_t sum; + uint8_t ctrl; + + switch(test) + { + case 0: + m("Signature \"USBPower\" \"########\""); + strncpy(status + 28, h->magic, 8); + return strncmp(h->magic, "USBPower", 8) ? 2:0; + case 1: + m("MCS Type 0xf3 0x%02x", h->mcs_type); + return (h->mcs_type != 0xf3) ? 2:0; + case 2: + m("Sequence 1 0x0010001000 0x%02x%02x%02x%02x%02x", + h->seq1[0], h->seq1[1], h->seq1[2], h->seq1[3], h->seq1[4]); + return strncmp((const char *)h->seq1, "\x00\x01\x00\x01\x00", + 5) ? 1:0; + case 3: + ctrl = raw[0x13] + 0x41; + m("Control 1 0x%02x 0x%02x", ctrl, h->control1); + return (h->control1 != ctrl) ? 2:0; + case 4: + m("Sequence 2 0x01 0x%02x", h->seq2); + return (h->seq2 != 0x01) ? 1:0; + case 5: + m("File size 1 %-8zu %u", size, + be32toh(h->filesize_be1)); + return (be32toh(h->filesize_be1) != size) ? 2:0; + case 6: + ctrl = raw[0x13] + 0xb8; + m("Control 2 0x%02x 0x%02x", ctrl, h->control2); + return (h->control2 != ctrl) ? 2:0; + case 7: + sum = checksum(g1a, size); + m("Checksum 0x%02x 0x%02x", sum, + be16toh(h->checksum)); + return (be16toh(h->checksum) != sum) ? 2:0; + case 8: + m("File size 2 %-8zu %u", size, + be32toh(h->filesize_be2)); + return (be32toh(h->filesize_be2) != size) ? 2:0; + default: + return -1; + } +} + +/* unknown(): Print an unknown field + @data Address of field + @offset Offset of field in header + @size Number of consecutive unknown bytes */ +static void unknown(uint8_t const *data, size_t offset, size_t size) +{ + printf(" 0x%03zx %-4zd 0x", offset, size); + for(size_t i = 0; i < size; i++) printf("%02x", data[offset + i]); + printf("\n"); +} + +/* field(): Print a text field with limited size + @field Address of text field + @size Maximum number of bytes to print */ +static void field(const char *field, size_t size) +{ + for(size_t i = 0; i < size && field[i]; i++) putchar(field[i]); + printf("\n"); +} + +/* dump(): Print the detailed header fields of a g1a file */ +void dump(struct g1a const *g1a, size_t size) +{ + struct header const *header = &g1a->header; + uint8_t const *raw = (void *)header; + + /* Checks for g1a files */ + char status[81]; + int ret = 0; + int passed = 0; + + printf("G1A signature checks:\n\n"); + printf(" Sta. Field Expected Value\n"); + + for(int test = 0; ret >= 0; test++) + { + ret = check(test, g1a, size, status); + passed += !ret; + if(ret < 0) break; + + printf(" %s %s\n", ret ? "FAIL" : "OK ", status); + } + + printf("\nFields with unknown meanings:\n\n"); + printf(" Offset Size Value\n"); + + unknown(raw, 0x015, 1); + unknown(raw, 0x018, 6); + unknown(raw, 0x028, 3); + unknown(raw, 0x02c, 4); + unknown(raw, 0x03a, 2); + unknown(raw, 0x04a, 2); + unknown(raw, 0x1d0, 4); + unknown(raw, 0x1dc, 20); + unknown(raw, 0x1f4, 12); + + printf("\nApplication metadata:\n\n"); + + printf(" Program name: "); + field(header->name, 8); + printf(" Internal name: "); + field(header->internal, 8); + printf(" Version: "); + field(header->version, 10); + printf(" Build date: "); + field(header->date, 14); + + printf("\nProgram icon:\n\n"); + icon_print(header->icon); +} diff --git a/fxg1a/edit.c b/fxg1a/edit.c new file mode 100644 index 0000000..60286ad --- /dev/null +++ b/fxg1a/edit.c @@ -0,0 +1,71 @@ +#include +#include +#include + +/* sign(): Sign header by filling fixed fields and checksums */ +void sign(struct g1a *g1a, size_t size) +{ + struct header *header = &g1a->header; + + /* Fixed elements */ + + memcpy(header->magic, "USBPower", 8); + header->mcs_type = 0xf3; + memcpy(header->seq1, "\x00\x10\x00\x10\x00", 5); + header->seq2 = 0x01; + + header->filesize_be1 = htobe32(size); + header->filesize_be2 = htobe32(size); + + /* Control bytes and checksums */ + + header->control1 = size + 0x41; + header->control2 = size + 0xb8; + header->checksum = htobe16(checksum(g1a, size)); +} + +/* edit_name(): Set application name */ +void edit_name(struct g1a *g1a, const char *name) +{ + memset(g1a->header.name, 0, 8); + if(!name) return; + + for(int i = 0; name[i] && i < 8; i++) + g1a->header.name[i] = name[i]; +} + +/* edit_internal(): Set internal name */ +void edit_internal(struct g1a *g1a, const char *internal) +{ + memset(g1a->header.internal, 0, 8); + if(!internal) return; + + for(int i = 0; internal[i] && i < 8; i++) + g1a->header.internal[i] = internal[i]; +} + +/* edit_version(): Set version string */ +void edit_version(struct g1a *g1a, const char *version) +{ + memset(g1a->header.version, 0, 10); + if(!version) return; + + for(int i = 0; version[i] && i < 10; i++) + g1a->header.version[i] = version[i]; +} + +/* edit_date(): Set build date */ +void edit_date(struct g1a *g1a, const char *date) +{ + memset(g1a->header.date, 0, 14); + if(!date) return; + + for(int i = 0; date[i] && i < 14; i++) + g1a->header.date[i] = date[i]; +} + +/* edit_icon(): Set icon from monochrome bitmap */ +void edit_icon(struct g1a *g1a, uint8_t const *mono) +{ + memcpy(g1a->header.icon, mono, 68); +} diff --git a/fxg1a/file.c b/fxg1a/file.c new file mode 100644 index 0000000..da59116 --- /dev/null +++ b/fxg1a/file.c @@ -0,0 +1,111 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include + +/* invert_header(): Bit-invert a standard header + Part of the header is stored inverted in files for obfuscation purposes. */ +static void invert_header(struct g1a *g1a) +{ + uint8_t *data = (void *)&g1a->header; + for(size_t i = 0; i < 0x20; i++) data[i] = ~data[i]; +} + +#define fail(msg, ...) { \ + fprintf(stderr, "error: " msg ": %m\n", ##__VA_ARGS__); \ + close(fd); \ + free(data); \ + return NULL; \ +} + +/* load(): Fully load a file into memory + Allocates a buffer with @prepend leading bytes initialized to zero. */ +static void *load(const char *filename, size_t *size, size_t prepend) +{ + int fd; + struct stat statbuf; + void *data = NULL; + size_t filesize; + + fd = open(filename, O_RDONLY); + if(fd < 0) fail("cannot open %s", filename); + + int x = fstat(fd, &statbuf); + if(x > 0) fail("cannot stat %s", filename); + + filesize = statbuf.st_size; + data = malloc(prepend + filesize); + if(!data) fail("cannot load %s", filename); + + size_t remaining = filesize; + while(remaining > 0) + { + size_t offset = prepend + filesize - remaining; + ssize_t y = read(fd, data + offset, remaining); + + if(y < 0) fail("cannot read from %s", filename); + remaining -= y; + } + close(fd); + + memset(data, 0, prepend); + + if(size) *size = prepend + filesize; + return data; +} + +/* load_g1a(): Load a g1a file into memory */ +struct g1a *load_g1a(const char *filename, size_t *size) +{ + struct g1a *ret = load(filename, size, 0); + if(ret) invert_header(ret); + return ret; +} + +/* load_binary(): Load a binary file into memory */ +struct g1a *load_binary(const char *filename, size_t *size) +{ + struct g1a *ret = load(filename, size, 0x200); + if(ret) memset(ret, 0xff, 0x20); + return ret; +} + +#undef fail +#define fail(msg, ...) { \ + fprintf(stderr, "error: " msg ": %m\n", ##__VA_ARGS__); \ + close(fd); \ + invert_header(g1a); \ + return 1; \ +} + +/* save_g1a(): Save a g1a file to disk */ +int save_g1a(const char *filename, struct g1a *g1a, size_t size) +{ + /* Invert header before saving */ + invert_header(g1a); + + int fd = creat(filename, 0644); + if(fd < 0) fail("cannot open %s", filename); + + void const *raw = g1a; + ssize_t status; + + size_t written = 0; + while(written < size) + { + status = write(fd, raw + written, size - written); + if(status < 0) fail("cannot write to %s", filename); + written += status; + } + close(fd); + + /* Before returning, re-invert header for further use */ + invert_header(g1a); + return 0; +} diff --git a/fxg1a/fxg1a.h b/fxg1a/fxg1a.h new file mode 100644 index 0000000..b084fa6 --- /dev/null +++ b/fxg1a/fxg1a.h @@ -0,0 +1,168 @@ +//--- +// fxg1a:fxg1a - Main interfaces +//--- + +#ifndef FX_FXG1A +#define FX_FXG1A + +#include +#include +#include +#include + +/* +** Header dumping (dump.c) +*/ + +/* dump(): Print the detailed header fields of a g1a file + This function takes as argument the full file loaded into memory and the + size of the file. It does various printing to stdout as main job. + + @g1a Full file data + @size Size of g1a file */ +void dump(struct g1a const *g1a, size_t size); + + +/* +** Header manipulation (edit.c) +*/ + +/* sign(): Sign header by filling fixed fields and checksums + This function fills the fixed fields and various checksums of a g1a file. To + do this it accesses some of the binary data. To set the user-customizable + field, use the edit_*() functions. (The value of the customizable fields + does not influence the checksums so it's okay to not call this function + afterwards.) + + @g1a Header to sign + @size Size of raw file data */ +void sign(struct g1a *g1a, size_t size); + +/* edit_*(): Set various fields of a g1a header */ + +void edit_name (struct g1a *g1a, const char *name); +void edit_internal (struct g1a *g1a, const char *internal); +void edit_version (struct g1a *g1a, const char *version); +void edit_date (struct g1a *g1a, const char *date); + +/* edit_icon(): Set monochrome icon of a g1a header + The icon parameter must be loaded in 1-bit bitmap format. */ +void edit_icon(struct g1a *header, uint8_t const *mono); + + +/* +** Utility functions (util.c) +*/ + +/* checksum(): Sum of 8 big-endian shorts at 0x300 + Computes the third checksum by summing bytes from the data part of the file. + + @g1a Add-in file whose checksum is requested + @size Size of file */ +uint16_t checksum(struct g1a const *g1a, size_t size); + +/* default_output(): Calculate default output file name + This function computes a default file name by replacing the extension of + @name (if it exists) or adding one. The extension is specified as a suffix, + usually in the form ".ext". + + The resulting string might be as long as the length of @name plus that of + @suffix (plus one NUL byte); the provided buffer must point to a suitably- + large allocated space. + + @name Input file name + @suffix Suffix to add or replace @name's extension with + @output Output file name */ +void default_output(const char *name, const char *suffix, char *output); + + +/* +** File manipulation (file.c) +*/ + +/* load_g1a(): Load a g1a file into memory + This function loads @filename into a dynamically-allocated buffer and + returns the address of that buffer; it must be free()'d after use. When + loading the file, if @size is not NULL, it receives the size of the file. + On error, load() prints a message an stderr and returns NULL. The header + is inverted before this function returns. + + @filename File to load + @size If non-NULL, receives the file size + Returns a pointer to a buffer with loaded data, or NULL on error. */ +struct g1a *load_g1a(const char *filename, size_t *size); + +/* load_binary(): Load a binary file into memory + This function operates like load_g1a() but reserves space for an empty + header. The header is initialized with all zeros. + + @filename File to load + @size If non-NULL, receives the file size + Returns a pointer to a buffer with loaded data, or NULL on error. */ +struct g1a *load_binary(const char *filename, size_t *size); + +/* save_g1a(): Save a g1a file to disk + This functions creates @filename, then writes a g1a header and a chunk of + raw data to it. Since it temporarily inverts the header to comply with + Casio's obfuscated format, it needs write access to @g1a. Returns non-zero + on error. + + @filename File to write (it will be overridden if it exists) + @g1a G1A data to write + @size Size of data + Returns zero on success and a nonzero error code otherwise. */ +int save_g1a(const char *filename, struct g1a *g1a, size_t size); + + +/* +** Icon management (icon.c) +*/ + +/* icon_print(): Show a monochrome 30*17 icon on stdout + The buffer should point to a 68-byte array. */ +void icon_print(uint8_t const *icon); + +/* icon_load(): Load a monochrome PNG image into an array + This function loads a PNG image into a 1-bit buffer; each row is represented + by a fixed number of bytes, each byte being 8 pixels. Rows are loaded from + top to bottom, and from left to right. + + If the image is not a PNG image or a reading error occurs, this functions + prints an error message on stderr and returns NULL. + + @filename PNG file to load + @width If non-NULL, receives image width + @height If non-NULL, receives image height + Returns a pointer to a free()able buffer with loaded data, NULL on error. */ +uint8_t *icon_load(const char *filename, size_t *width, size_t *height); + +/* icon_save(): Save an 8-bit array to a PNG image + Assumes 8-bit GRAY format. + + @filename Target filename + @input An 8-bit GRAY image + @width Width of input, should be equal to stride + @height Height of input + Returns non-zero on error. */ +int icon_save(const char *filename, uint8_t *input, size_t width, + size_t height); + +/* icon_conv_8to1(): Convert an 8-bit icon to 1-bit + The returned 1-bit icon is always of size 30*17, if the input size does not + match it is adjusted. + + @input 8-bi data + @width Width of input image + @height Height of input image + Returns a free()able buffer with a 1-bit icon on success, NULL on error. */ +uint8_t *icon_conv_8to1(uint8_t const *input, size_t width, size_t height); + +/* icon_conv_1to8(): Convert an 1-bit icon to 8-bit + Input 1-bit is assumed to be 30*17 in size, this function returns an 8-bit + buffer with the same dimensions. + + @mono Input monochrome icon (from a g1a header, for instance) + Returns a free()able buffer, or NULL on error. */ +uint8_t *icon_conv_1to8(uint8_t const *mono); + +#endif /* FX_FXG1A */ diff --git a/fxg1a/g1a.h b/fxg1a/g1a.h new file mode 100644 index 0000000..862c74b --- /dev/null +++ b/fxg1a/g1a.h @@ -0,0 +1,59 @@ +//--- +// fxg1a:g1a - Add-in header for Casio's G1A format +//--- + +#ifndef FX_G1A +#define FX_G1A + +#include + +/* TODO: eStrips are not supported yet */ +struct estrip +{ + uint8_t data[80]; +}; + +/* G1A file header with 0x200 bytes. When output to a file the standard part + (first 0x20 bytes) of this header is bit-inverted, but the non-inverted + version makes a lot more sens so we'll be using it. */ +struct header +{ /* Offset Size Value */ + char magic[8]; /* 0x000 8 "USBPower" */ + uint8_t mcs_type; /* 0x008 1 0xf3 (AddIn) */ + uint8_t seq1[5]; /* 0x009 5 0x0010001000 */ + uint8_t control1; /* 0x00e 1 *0x13 + 0x41 */ + uint8_t seq2; /* 0x00f 1 0x01 */ + uint32_t filesize_be1; /* 0x010 4 File size, big endian */ + uint8_t control2; /* 0x014 1 *0x13 + 0xb8 */ + uint8_t _1; /* 0x015 1 ??? */ + uint16_t checksum; /* 0x016 2 BE sum of 8 shorts at 0x300 */ + uint8_t _2[6]; /* 0x018 6 ??? */ + uint16_t mcs_objects; /* 0x01e 2 MCS-only, unused */ + char internal[8]; /* 0x020 8 Internal app name with '@' */ + uint8_t _3[3]; /* 0x028 3 ??? */ + uint8_t estrips; /* 0x02b 1 Number of estrips (0..4) */ + uint8_t _4[4]; /* 0x02c 4 ??? */ + char version[10]; /* 0x030 10 Version "MM.mm.pppp" */ + uint8_t _5[2]; /* 0x03a 2 ??? */ + char date[14]; /* 0x03c 14 Build date "yyyy.MMdd.hhmm" */ + uint8_t _6[2]; /* 0x04a 2 ??? */ + uint8_t icon[68]; /* 0x04c 68 30*17 monochrome icon */ + struct estrip estrip1; /* 0x090 80 eStrip 1 */ + struct estrip estrip2; /* 0x0e0 80 eStrip 2 */ + struct estrip estrip3; /* 0x130 80 eStrip 3 */ + struct estrip estrip4; /* 0x180 80 eStrip 4 */ + uint8_t _7[4]; /* 0x1d0 4 ??? */ + char name[8]; /* 0x1d4 8 Add-in name */ + uint8_t _8[20]; /* 0x1dc 20 ??? */ + uint32_t filesize_be2; /* 0x1f0 4 File size, big endian */ + uint8_t _9[12]; /* 0x1f4 12 ??? */ +}; + +/* A full g1a file, suitable for use with pointers */ +struct g1a +{ + struct header header; + uint8_t code[]; +}; + +#endif /* FX_G1A */ diff --git a/fxg1a/icon.c b/fxg1a/icon.c new file mode 100644 index 0000000..79a21ee --- /dev/null +++ b/fxg1a/icon.c @@ -0,0 +1,132 @@ +#include +#include + +#include +#include + +/* icon_print(): Show a monochrome 30*17 icon on stdout */ +void icon_print(uint8_t const *icon) +{ + for(int y = 0; y < 17; y++) + { + for(int x = 0; x < 30; x++) + { + int v = icon[(y << 2) + (x >> 3)] & (0x80 >> (x & 7)); + putchar(v ? '#' : ' '); + putchar(v ? '#' : ' '); + } + + putchar('\n'); + } +} + +/* icon_load(): Load a monochrome PNG image into an array */ +uint8_t *icon_load(const char *filename, size_t *width, size_t *height) +{ + png_image img; + memset(&img, 0, sizeof img); + img.opaque = NULL; + img.version = PNG_IMAGE_VERSION; + + png_image_begin_read_from_file(&img, filename); + if(img.warning_or_error) + { + fprintf(stderr, "libpng %s: %s\n", img.warning_or_error == 1 + ? "warning": "error", img.message); + if(img.warning_or_error > 1) + { + png_image_free(&img); + return NULL; + } + } + + img.format = PNG_FORMAT_GRAY; + + void *buffer = calloc(img.width * img.height, 1); + if(!buffer) + { + fprintf(stderr, "error: cannot read %s: %m\n", filename); + png_image_free(&img); + return NULL; + } + + png_image_finish_read(&img, NULL, buffer, img.width, NULL); + if(width) *width = img.width; + if(height) *height = img.height; + + png_image_free(&img); + return buffer; +} + +/* icon_save(): Save an 8-bit array to a PNG image */ +int icon_save(const char *filename, uint8_t *input, size_t width, + size_t height) +{ + png_image img; + memset(&img, 0, sizeof img); + + img.version = PNG_IMAGE_VERSION; + img.width = width; + img.height = height; + img.format = PNG_FORMAT_GRAY; + + png_image_write_to_file(&img, filename, 0, input, 0, NULL); + png_image_free(&img); + + if(img.warning_or_error) + { + fprintf(stderr, "libpng %s: %s\n", img.warning_or_error == 1 + ? "warning": "error", img.message); + if(img.warning_or_error > 1) return 1; + } + + return 0; +} + +/* icon_conv_8to1(): Convert an 8-bit icon to 1-bit */ +uint8_t *icon_conv_8to1(uint8_t const *input, size_t width, size_t height) +{ + if(!input) return NULL; + uint8_t *mono = calloc(68, 1); + if(!mono) return NULL; + size_t stride = width; + + /* If the image is wider than 30 pixels, ignore columns at the right */ + if(width > 30) width = 30; + + /* Skip the first line if there is enough rows, because in standard + 30*19 icons, the first and last lines are skipped */ + if(height > 17) input += stride, height--; + + /* If height is still larger than 17, ignore rows at the bottom */ + if(height > 17) height = 17; + + /* Then copy individual pixels on the currently-blank image */ + for(size_t y = 0; y < height; y++) + for(size_t x = 0; x < width; x++) + { + int offset = (y << 2) + (x >> 3); + int color = input[y * stride + x] < 128; + uint8_t mask = color << (~x & 7); + mono[offset] |= mask; + } + + return mono; +} + +/* icon_conv_1to8(): Convert an 1-bit icon to 8-bit */ +uint8_t *icon_conv_1to8(uint8_t const *mono) +{ + uint8_t *data = calloc(30 * 17, 1); + if(!data) return NULL; + + for(int y = 0; y < 17; y++) + for(int x = 0; x < 30; x++) + { + int offset = (y << 2) + (x >> 3); + int bit = mono[offset] & (0x80 >> (x & 7)); + data[y * 30 + x] = (bit ? 0x00 : 0xff); + } + + return data; +} diff --git a/fxg1a/main.c b/fxg1a/main.c new file mode 100644 index 0000000..c45ed29 --- /dev/null +++ b/fxg1a/main.c @@ -0,0 +1,266 @@ +#include +#include +#include +#include + +#include +#include + +static const char *help_string = +"usage: %1$s [-g] [options...]\n" +" %1$s -e [options...]\n" +" %1$s -d \n" +" %1$s -r [-o ]\n" +" %1$s -x [-o ]\n" +"\n" +"fxg1a creates or edits g1a files (add-in applications for Casio fx9860g\n" +"calculator series) that consist of a g1a header followed by binary code.\n" +"\n" +"Operating modes:\n" +" -g, --g1a Generate a g1a file (default)\n" +" -e, --edit Edit header of an existing g1a file\n" +" -d, --dump Dump header of an existing g1a file\n" +" -r, --repair Recalculate control bytes and checksums\n" +" -x, --extract Extract icon into a PNG file\n" +"\n" +"General options:\n" +" -o, --output= Output file (default: input file with .g1a suffix\n" +" [-g]; with .png suffix [-x]; input file [-e, -r])\n" +"\n" +"Generation and edition options:\n" +" -i, --icon= Program icon, in PNG format (default: blank icon)\n" +" -n, --name= Add-in name, 8 bytes (default: output file name)\n" +" --version= Program version, MM.mm.pppp format (default: empty)\n" +" --internal= Internal name, eg. '@NAME' (default: empty)\n" +" --date= Date of build, yyyy.MMdd.hhmm (default: now)\n"; + +/* +** Field customization +*/ + +/* A set of user-defined fields, often taken on the command-line + Default values are NULL and indicate "no value" (-g) or "no change" (-e). */ +struct fields +{ + /* New values for basic fields */ + const char *name; + const char *version; + const char *internal; + const char *date; + /* Icon file name */ + const char *icon; +}; + +/* fields_edit(): Set the value of some fields altogether + @header Header to edit, is assumed checksumed and filled + @fields New values for fields, any members can be NULL */ +void fields_edit(struct g1a *header, struct fields const *fields) +{ + /* For easy fields, just call the appropriate edition function */ + if(fields->name) edit_name(header, fields->name); + if(fields->version) edit_version(header, fields->version); + if(fields->internal) edit_internal(header, fields->internal); + if(fields->date) edit_date(header, fields->date); + + /* Load icon from PNG file */ + if(fields->icon) + { + size_t width, height; + uint8_t *data = icon_load(fields->icon, &width, &height); + if(!data) return; + + uint8_t *mono = icon_conv_8to1(data, width, height); + free(data); + + if(!mono) return; + edit_icon(header, mono); + } +} + +/* +** Tool implementation +*/ + +int main(int argc, char **argv) +{ + /* Result of option parsing */ + int mode = 'g', error = 0; + struct fields fields = { 0 }; + const char *output = NULL; + + const struct option longs[] = { + { "help", no_argument, NULL, 'h' }, + { "g1a", no_argument, NULL, 'g' }, + { "edit", no_argument, NULL, 'e' }, + { "dump", no_argument, NULL, 'd' }, + { "repair", no_argument, NULL, 'r' }, + { "extract", no_argument, NULL, 'x' }, + { "output", required_argument, NULL, 'o' }, + { "icon", required_argument, NULL, 'i' }, + { "name", required_argument, NULL, 'n' }, + { "version", required_argument, NULL, 'v' }, + { "internal", required_argument, NULL, 't' }, + { "date", required_argument, NULL, 'a' }, + { NULL, 0, NULL, 0 }, + }; + + int option = 0; + while(option >= 0 && option != '?') + switch((option = getopt_long(argc, argv, "hgedrxo:i:n:", longs, NULL))) + { + case 'h': + fprintf(stderr, help_string, argv[0]); + return 0; + case 'g': + case 'e': + case 'd': + case 'r': + case 'x': + mode = option; + break; + case 'o': + output = optarg; + break; + case 'i': + fields.icon = optarg; + break; + case 'n': + fields.name = optarg; + break; + case 'v': + fields.version = optarg; + break; + case 't': + fields.internal = optarg; + break; + case 'a': + fields.date = optarg; + break; + case '?': + error = 1; + break; + } + + if(error) return 1; + + if(argv[optind] == NULL) + { + fprintf(stderr, help_string, argv[0]); + return 1; + } + if(mode == 'g') + { + /* Load binary file into memory */ + size_t size; + struct g1a *g1a = load_binary(argv[optind], &size); + if(!g1a) return 1; + + /* If [output] is set, use it, otherwise compute a default */ + char *alloc = NULL; + if(!output) + { + alloc = malloc(strlen(argv[optind]) + 5); + if(!alloc) {fprintf(stderr, "error: %m\n"); return 1;} + default_output(argv[optind], ".g1a", alloc); + } + + /* Start with output file name as application name */ + edit_name(g1a, output ? output : alloc); + + /* Start with "now" as build date */ + char date[15]; + time_t t = time(NULL); + struct tm *now = localtime(&t); + strftime(date, 15, "%Y.%m%d.%H%M", now); + edit_date(g1a, date); + + /* Edit the fields with user-customized values */ + fields_edit(g1a, &fields); + + /* Set fixed fields and calculate checksums */ + sign(g1a, size); + + save_g1a(output ? output : alloc, g1a, size); + free(alloc); + + /* Write output file */ + free(g1a); + } + if(mode == 'e') + { + /* Load g1a file into memory */ + size_t size; + struct g1a *g1a = load_g1a(argv[optind], &size); + if(!g1a) return 1; + + /* Edit the fields with user-customized values */ + fields_edit(g1a, &fields); + + /* We don't reset fixed fields or recalculate checksums because + we only want to edit what was requested by the user. + Besides, the control bytes and checksums do *not* depend on + the value of user-customizable fields. */ + + /* Regenerate input file, or output somewhere else */ + if(!output) output = argv[optind]; + save_g1a(output, g1a, size); + free(g1a); + } + if(mode == 'd') + { + /* Load and dump the g1a */ + size_t size; + struct g1a *g1a = load_g1a(argv[optind], &size); + if(!g1a) return 1; + + dump(g1a, size); + free(g1a); + } + if(mode == 'r') + { + /* Load g1a file into memory */ + size_t size; + struct g1a *g1a = load_g1a(argv[optind], &size); + if(!g1a) return 1; + + /* Repair file by recalculating fixed fields and checksums */ + sign(g1a, size); + + /* Regenerate input file, or output somewhere else */ + if(!output) output = argv[optind]; + save_g1a(output, g1a, size); + free(g1a); + } + if(mode == 'x') + { + /* Load g1a file into memory */ + size_t size; + struct g1a *g1a = load_g1a(argv[optind], &size); + if(!g1a) return 1; + + /* Generate 8-bit icon from g1a 1-bit */ + uint8_t *data = icon_conv_1to8(g1a->header.icon); + if(!data) + { + fprintf(stderr, "error: %m\n"); + return 1; + } + + /* Calculate a default output name if none is provided */ + if(output) + { + icon_save(output, data, 30, 17); + } + else + { + char *alloc = malloc(strlen(argv[optind]) + 5); + if(!alloc) {fprintf(stderr, "error: %m\n"); return 1;} + default_output(argv[optind], ".png", alloc); + + icon_save(alloc, data, 30, 17); + free(alloc); + } + } + + return 0; +} diff --git a/fxg1a/util.c b/fxg1a/util.c new file mode 100644 index 0000000..7dc3b72 --- /dev/null +++ b/fxg1a/util.c @@ -0,0 +1,47 @@ +#include +#include + +/* +** Public API +*/ + +/* checksum(): Sum of 8 big-endian shorts at 0x300 */ +uint16_t checksum(struct g1a const *g1a, size_t size) +{ + uint16_t shorts[16] = { 0 }; + + /* Extract 16 bytes from the file (maybe less are available) */ + int available = size - 0x300; + if(available < 0) available = 0; + if(available > 16) available = 16; + memcpy(shorts, g1a->code + 0x100, available); + + /* Do the big-endian sum */ + uint16_t sum = 0; + for(int i = 0; i < 8; i++) sum += htobe16(shorts[i]); + + return sum; +} + +/* default_output(): Calculate default output file name */ +void default_output(const char *name, const char *suffix, char *output) +{ + /* Check if there is a dot at the end of @name, before the last '/'. + The dot must also not be in first position (hidden files) */ + size_t end = strlen(name) - 1; + while(end >= 1 && name[end] != '/' && name[end] != '.') end--; + + /* If we don't have a dot in the file name, append the extension */ + if(end < 1 || name[end] != '.') + { + strcpy(output, name); + strcat(output, suffix); + } + + /* If we found a dot before the last slash, replace the extension */ + else + { + memcpy(output, name, end); + strcpy(output + end, suffix); + } +} diff --git a/fxos/fxos.h b/fxos/fxos.h new file mode 100644 index 0000000..5c5f2a6 --- /dev/null +++ b/fxos/fxos.h @@ -0,0 +1,154 @@ +//--- +// fxos:fxos - Main interfaces +//--- + +#ifndef FX_FXOS +#define FX_FXOS + +#include +#include + +/* Microprocessor platforms */ +enum mpu +{ + mpu_unknown = 0, + mpu_sh7705 = 1, + mpu_sh7305 = 2, +}; + + +/* +** Data tables (tables.c) +*/ + +/* tables_add_asm(): Append an instruction table to the table list + This function adds a new instruction table to fetch instructions from; it + will be consulted if searching any of the previously-declared tables fails. + + @filename Name of instruction table file + Returns non-zero on error (and prints a message on stderr) */ +int tables_add_asm(const char *filename); + +/* tables_add_syscall(): Append a syscall table to the table list + This function adds a new syscall table to fetch syscalls information from; + if will be consulted if searching any of the previously-declared tables + fails. + + @filename Name of instruction table file + Returns non-zero on error (and prints a message on stderr) */ +int tables_add_syscall(const char *filename); + + +/* +** RAM dumps (ram.c) +*/ + +/* Region for a single RAM dump */ +struct region +{ + uint32_t start; + uint32_t length; + void *data; +}; + +/* RAM dump */ +struct ram_sh7705 +{ + struct region RAM; /* Usual RAM (256k) */ +}; +struct ram_sh7305 +{ + struct region RAM; /* Usual RAM (512k) */ + struct region IL; /* On-chip instruction storage (16k) */ + struct region RS; /* On-chip generic storage (2k) */ +}; + + +/* +** File identification (info.c) +*/ + +/* Info options */ +struct info +{ + /* OS file (0) or binary file (1) */ + int binary; + /* Force underlying architecture */ + enum mpu mpu; + + /* RAM dumps, if any */ + union { + struct ram_sh7705 ram3; + struct ram_sh7305 ram4; + }; +}; + + +/* +** Disassembling (disassembly.c) +*/ + +/* Disassembly options */ +struct disassembly +{ + /* OS file (0) or binary file (1) */ + int binary; + /* Force underlying architecture */ + enum mpu mpu; + + /* RAM dumps, if any */ + union { + struct ram_sh7705 ram3; + struct ram_sh7305 ram4; + }; + + /* Start address */ + uint32_t start; + /* Length of disassembled region */ + uint32_t length; +}; + + +/* +** Blind analysis (analysis.c) +*/ + +/* Analysis options */ +struct analysis +{ + /* Force underlying architecture */ + enum mpu mpu; + + /* RAM dumps, if any */ + union { + struct ram_sh7705 ram3; + struct ram_sh7305 ram4; + }; + + /* Analysis mode */ + enum { + ANALYSIS_FULL = 0, + ANALYSIS_SYSCALL = 1, + ANALYSIS_ADDRESS = 2, + ANALYSIS_REGISTER = 3, + } type; + + /* Max number of printed occurrences */ + int occurrences; +}; + + +/* +** Utility functions (util.c) +*/ + +/* integer(): Convert base 8, 10 or 16 into integers + Prints an error message and sets *error to 1 in case of conversion error or + overflow. + + @str Original integer representation ("10", "0x1f", "07") + @error Set to 1 on error, otherwise unchanged (can be NULL) + Returns result of conversion (valid if *error is not 1) */ +long long integer(const char *str, int *error); + +#endif /* FX_FXOS */ diff --git a/fxos/main.c b/fxos/main.c new file mode 100644 index 0000000..cc364fd --- /dev/null +++ b/fxos/main.c @@ -0,0 +1,183 @@ +#include +#include +#include +#include + +#include + +static const char *help_string = +"usage: %1$s info ( | -b )\n" +" %1$s disasm (-a
| -s ) [options...]\n" +" %1$s disasm -b [options...]\n" +" %1$s analyze [-f|-s|-a|-r] [options...]\n" +"\n" +"fxos disassembles or analyzes binary and OS files for efficient reverse-\n" +"engineering. It currently only supports fx9860g binaries.\n" +"\n" +"Commands:\n" +" info Identify an OS image: version, platform, date, checksums...\n" +" Identify the architecture of a binary file.\n" +" disasm Disassemble and annotate code with relative address targets,\n" +" syscall invocations and hints about memory structure.\n" +" analyze Dig an address or syscall number, finding syscall call graph,\n" +" 4-aligned occurrences, memory region and probable role.\n" +"\n" +"General options:\n" +" -b Disassemble any binary file, not an OS file\n" +" -3, --sh3 Assume SH3 OS and platform (default: guess)\n" +" -4, --sh4 Assume SH4 OS and platform (default: guess)\n" +" --ram Read RAM dumps from \n" +" --table-asm Read more instruction patterns in \n" +" --table-call Read more syscall prototypes in \n" +"\n" +"Disassembly region options:\n" +" -a
Start disassembling at this address\n" +" -s Start disassembling at this syscall's address\n" +" -l Length of region\n" +"\n" +"Analysis modes:\n" +" -f, --full Find everything that can be known about \n" +" -s, --syscall is a syscall ID\n" +" -a, --address is a code/data address in ROM or RAM\n" +" -r, --register is a register or peripheral address\n" +"\n" +"Analysis options:\n" +" --occurrences Show at most occurrences (integer or \"all\")\n"; + +/* "Low-level" command-line option set */ +struct options +{ + const char *input; + + int target; + const char *ram; + + const char *a; + const char *s; + size_t l; + + int f; + int r; + const char *occ; +}; + +int main(int argc, char **argv) +{ + int command = 0, error = 0; + struct options opt = { 0 }; + /* For string -> int conversions, first non-int character */ + + + /* Get command name */ + if(argc >= 2) + { + if(!strcmp(argv[1], "info")) command = 'i'; + if(!strcmp(argv[1], "disasm")) command = 'd'; + if(!strcmp(argv[1], "analyze")) command = 'a'; + + if(!command && argv[1][0] != '-') + { + fprintf(stderr, "invalid operation: '%s'\n", argv[1]); + fprintf(stderr, "Try '%s --help'.\n", argv[0]); + return 1; + } + + if(command) argv[1] = ""; + } + + enum { + OPT_RAM = 1, + OPT_ASM = 2, + OPT_CALL = 3, + OPT_OCC = 4, + }; + const struct option longs[] = { + { "help", no_argument, NULL, 'h' }, + { "sh3", no_argument, NULL, '3' }, + { "sh4", no_argument, NULL, '4' }, + { "ram", required_argument, NULL, OPT_RAM }, + { "table-asm", required_argument, NULL, OPT_ASM }, + { "table-call", required_argument, NULL, OPT_CALL }, + { "full", no_argument, NULL, 'f' }, + { "syscall", no_argument, NULL, 's' }, + { "address", no_argument, NULL, 'a' }, + { "register", no_argument, NULL, 'r' }, + { "occurrences", required_argument, NULL, OPT_OCC }, + }; + + int option = 0; + while(option >= 0 && option != '?') + switch((option = getopt_long(argc, argv, "hb34a::s::l:fr",longs,NULL))) + { + case 'h': + fprintf(stderr, help_string, argv[0]); + return 0; + case '3': + case '4': + opt.target = option; + break; + case OPT_RAM: + opt.ram = optarg; + break; + case OPT_ASM: + tables_add_asm(optarg); + break; + case OPT_CALL: + tables_add_syscall(optarg); + break; + case 'f': + opt.f = 1; + break; + case 's': + opt.s = optarg; + break; + case 'a': + opt.a = optarg; + break; + case 'l': + opt.l = integer(optarg, &error); + break; + case 'r': + opt.r = 1; + break; + case OPT_OCC: + opt.occ = optarg; + break; + case '?': + error = 1; + break; + } + + if(error) return 1; + opt.input = argv[optind]; + + if(!opt.input) + { + fprintf(stderr, help_string, argv[0]); + return 1; + } + + /* Load default tables (user-specified tables will override them) */ + tables_add_asm (FXSDK_PREFIX "/share/assembly-sh3.txt"); + tables_add_syscall(FXSDK_PREFIX "/share/syscalls.txt"); + + /* Change interpretation of arguments depending on mode */ + + printf("Operation is '%c'\n", command); + printf(" input=%s\n", opt.input); + printf(" target='%c'\n", opt.target); + printf(" ram=%s\n", opt.ram); + printf(" a=%s\n", opt.a); + printf(" s=%s\n", opt.s); + printf(" l=%zu\n", opt.l); + printf(" f=%d\n", opt.f); + printf(" r=%d\n", opt.r); + printf(" occ=%s\n", opt.occ); + + /* TODO: Execution procedure: + TODO: 1. Identify architecture + TODO: 2. Load RAM data, syscall tables, peripheral modules, etc + TODO: 3. Execute command */ + + return 0; +} diff --git a/fxos/tables.c b/fxos/tables.c new file mode 100644 index 0000000..032b05c --- /dev/null +++ b/fxos/tables.c @@ -0,0 +1,59 @@ +#include +#include + +/* A linked list of pointers (either file names or tables) */ +struct element +{ + void *data; + struct element *next; +}; + +/* list_append(): Append a pointer to a linked list + Returns a pointer to the new node, NULL on error. */ +struct element *list_append(struct element **head, void *data) +{ + struct element *el = malloc(sizeof *el); + if(!el) return NULL; + el->data = data; + el->next = NULL; + + while(*head) head = &((*head)->next); + *head = el; + + return el; +} + +/* list_free(): Free a linked list */ +void list_free(struct element *head, void (*destructor)(void *)) +{ + struct element *next; + while(head) + { + destructor(head->data); + next = head->next; + free(head); + head = next; + } +} + +/* Table of assembly instruction listings */ +static struct element *tasm = NULL; +/* Table of system call information listings */ +static struct element *tcall = NULL; + + +/* +** Public API +*/ + +/* tables_add_asm(): Append an instruction table to the table list */ +int tables_add_asm(const char *filename) +{ + return !list_append(&tasm, (void *)filename); +} + +/* tables_add_syscall(): Append a syscall table to the table list */ +int tables_add_syscall(const char *filename) +{ + return !list_append(&tcall, (void *)filename); +} diff --git a/fxos/util.c b/fxos/util.c new file mode 100644 index 0000000..2c380c9 --- /dev/null +++ b/fxos/util.c @@ -0,0 +1,30 @@ +#include +#include +#include +#include + +/* integer(): Convert base 8, 10 or 16 into integers */ +long long integer(const char *str, int *error) +{ + char *end; + errno = 0; + + long long ll = strtoll(str, &end, 0); + if(errno == ERANGE) + { + fprintf(stderr, "error: integer is too large: '%s'\n", str); + if(error) *error = 1; + + /* In my situation this is often better than LLONG_MIN/MAX */ + return 0; + } + + if(*end) + { + fprintf(stderr, "invalid integer: '%s'\n", str); + if(error) *error = 1; + return 0; + } + + return ll; +} diff --git a/fxsdk/main.c b/fxsdk/main.c new file mode 100644 index 0000000..31dbf45 --- /dev/null +++ b/fxsdk/main.c @@ -0,0 +1,4 @@ +int main(void) +{ + return 0; +}