Browse Source

fxsdk: initial push for gint v2 (WIP but mostly done)

master
Lephe 6 months ago
commit
d19025bbc9
18 changed files with 2287 additions and 0 deletions
  1. 17
    0
      .gitignore
  2. 119
    0
      Makefile
  3. 112
    0
      configure
  4. 112
    0
      fxconv/fxconv-main.py
  5. 497
    0
      fxconv/fxconv.py
  6. 146
    0
      fxg1a/dump.c
  7. 71
    0
      fxg1a/edit.c
  8. 111
    0
      fxg1a/file.c
  9. 168
    0
      fxg1a/fxg1a.h
  10. 59
    0
      fxg1a/g1a.h
  11. 132
    0
      fxg1a/icon.c
  12. 266
    0
      fxg1a/main.c
  13. 47
    0
      fxg1a/util.c
  14. 154
    0
      fxos/fxos.h
  15. 183
    0
      fxos/main.c
  16. 59
    0
      fxos/tables.c
  17. 30
    0
      fxos/util.c
  18. 4
    0
      fxsdk/main.c

+ 17
- 0
.gitignore View File

@@ -0,0 +1,17 @@
# Configuration file
Makefile.cfg

# Build directory
build/

# Binaries
bin/

# Test icons
icons/

# Documentation drafts
doc/

# Python cache
__pycache__

+ 119
- 0
Makefile View File

@@ -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

+ 112
- 0
configure View File

@@ -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:
<tool> 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-<tool> Build and install the selected tool [default]
--disable-<tool> Do not build or install the selected tool

Install folders:
Executables will be installed in <prefix>/bin and runtime data in
<prefix>/share/fxsdk.

--prefix=<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'."

+ 112
- 0
fxconv/fxconv-main.py View File

@@ -0,0 +1,112 @@
#! /usr/bin/python3

import getopt
import sys
import os
import fxconv

help_string = """
usage: fxconv [-s] <python script> [files...]
fxconv -b <bin file> -o <object file> [parameters...]
fxconv -i <png file> -o <object file> [parameters...]
fxconv -f <png file> -o <object file> [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()

+ 497
- 0
fxconv/fxconv.py View File

@@ -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:
* _<varname>
* _<varname>_end
* _<varname>_size
The variable name is obtained from the parameter dictionary <params>.

Arguments:
input -- Input file path
params -- Parameter dictionary
output -- Output file name [default: <input> 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:
* <symbol>
* <symbol>_end
* <symbol>_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}")

+ 146
- 0
fxg1a/dump.c View File

@@ -0,0 +1,146 @@
#include <stdio.h>
#include <string.h>
#include <endian.h>

#include <fxg1a.h>
#include <g1a.h>

/* 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);
}

+ 71
- 0
fxg1a/edit.c View File

@@ -0,0 +1,71 @@
#include <fxg1a.h>
#include <stdio.h>
#include <string.h>

/* 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);
}

+ 111
- 0
fxg1a/file.c View File

@@ -0,0 +1,111 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fxg1a.h>

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

+ 168
- 0
fxg1a/fxg1a.h View File

@@ -0,0 +1,168 @@
//---
// fxg1a:fxg1a - Main interfaces
//---

#ifndef FX_FXG1A
#define FX_FXG1A

#include <stdlib.h>
#include <stdint.h>
#include <stddef.h>
#include <g1a.h>

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

+ 59
- 0
fxg1a/g1a.h View File

@@ -0,0 +1,59 @@
//---
// fxg1a:g1a - Add-in header for Casio's G1A format
//---

#ifndef FX_G1A
#define FX_G1A

#include <stdint.h>

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

+ 132
- 0
fxg1a/icon.c View File

@@ -0,0 +1,132 @@
#include <stdio.h>
#include <string.h>

#include <fxg1a.h>
#include <png.h>

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

+ 266
- 0
fxg1a/main.c View File

@@ -0,0 +1,266 @@
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <getopt.h>

#include <fxg1a.h>
#include <g1a.h>

static const char *help_string =
"usage: %1$s [-g] <binary file> [options...]\n"
" %1$s -e <g1a file> [options...]\n"
" %1$s -d <g1a file>\n"
" %1$s -r <g1a file> [-o <g1a file>]\n"
" %1$s -x <g1a file> [-o <png file>]\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=<file> 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=<png> Program icon, in PNG format (default: blank icon)\n"
" -n, --name=<name> Add-in name, 8 bytes (default: output file name)\n"
" --version=<text> Program version, MM.mm.pppp format (default: empty)\n"
" --internal=<name> Internal name, eg. '@NAME' (default: empty)\n"
" --date=<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;
}

+ 47
- 0
fxg1a/util.c View File

@@ -0,0 +1,47 @@
#include <string.h>
#include <fxg1a.h>

/*
** 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);
}
}

+ 154
- 0
fxos/fxos.h View File

@@ -0,0 +1,154 @@
//---
// fxos:fxos - Main interfaces
//---

#ifndef FX_FXOS
#define FX_FXOS

#include <stdint.h>
#include <stddef.h>

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

+ 183
- 0
fxos/main.c View File

@@ -0,0 +1,183 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>

#include <fxos.h>

static const char *help_string =
"usage: %1$s info (<os file> | -b <binary file>)\n"
" %1$s disasm <os file> (-a <address> | -s <syscall id>) [options...]\n"
" %1$s disasm -b <binary file> [options...]\n"
" %1$s analyze [-f|-s|-a|-r] <number> <os file> [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 <folder> Read RAM dumps from <folder>\n"
" --table-asm <file> Read more instruction patterns in <file>\n"
" --table-call <file> Read more syscall prototypes in <file>\n"
"\n"
"Disassembly region options:\n"
" -a <address> Start disassembling at this address\n"
" -s <syscall id> Start disassembling at this syscall's address\n"
" -l <length> Length of region\n"
"\n"
"Analysis modes:\n"
" -f, --full Find everything that can be known about <number>\n"
" -s, --syscall <number> is a syscall ID\n"
" -a, --address <number> is a code/data address in ROM or RAM\n"
" -r, --register <number> is a register or peripheral address\n"
"\n"
"Analysis options:\n"
" --occurrences <num> Show at most <num> 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) */