Skip to content

Instantly share code, notes, and snippets.

@peterhellberg
Last active February 5, 2025 15:11
Show Gist options
  • Save peterhellberg/6402e95154d57ef8ac886f6b36c398d1 to your computer and use it in GitHub Desktop.
Save peterhellberg/6402e95154d57ef8ac886f6b36c398d1 to your computer and use it in GitHub Desktop.
A quick and dirty decompressor for Firefly Zero carts (Which are Zip files compressed using ZSTD) - https://docs.fireflyzero.com/internal/formats/#rom
package main
import (
"archive/zip"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/klauspost/compress/zstd"
)
func main() {
if err := run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func run(args []string) error {
in, err := parse(args)
if err != nil {
return err
}
r, err := zip.OpenReader(in.name)
if err != nil {
return err
}
r.RegisterDecompressor(zstd.ZipMethodWinZip, zstd.ZipDecompressor())
base := strings.TrimSuffix(in.name, ".zip")
if err := os.Mkdir(base, 0o755); err != nil {
return err
}
for _, f := range r.File {
fn := filepath.Join(base, f.Name)
ff, err := r.Open(f.Name)
if err != nil {
return err
}
data, err := io.ReadAll(ff)
if err != nil {
return err
}
if err := os.WriteFile(fn, data, 0o755); err != nil {
return err
}
}
return nil
}
type input struct {
exe string
name string
}
func parse(args []string) (input, error) {
var in input
if len(args) == 0 {
return in, fmt.Errorf("no arguments")
}
in.exe = args[0]
flags := flag.NewFlagSet(in.exe, flag.ExitOnError)
if err := flags.Parse(args[1:]); err != nil {
return in, err
}
if rest := flags.Args(); len(rest) > 0 {
in.name = rest[0]
}
if in.name == "" {
return in, fmt.Errorf("no zip file specified")
}
return in, nil
}
@peterhellberg
Copy link
Author

peterhellberg commented Feb 5, 2025

Blutti

$ wget https://github.com/ollej/firefly-blutti/releases/latest/download/olle.blutti.zip
$ unzipzstd olle.blutti.zip
$ file olle.blutti/*
olle.blutti/_badges:       Windows Precompiled iNF, version 1.3, InfStyle 1, flags 0x61747305, unicoded, src URL, at 0x73203030 "", LanguageID 0
olle.blutti/_bin:          WebAssembly (wasm) binary module version 0x1 (MVP)
olle.blutti/_hash:         data
olle.blutti/_meta:         data
olle.blutti/font:          0421 Alliant compact executable not stripped
olle.blutti/level1:        JSON data
olle.blutti/level2:        JSON data
olle.blutti/level3:        JSON data
olle.blutti/level4:        JSON data
olle.blutti/level5:        JSON data
olle.blutti/sound_coin:    data
olle.blutti/sound_dash:    data
olle.blutti/sound_death:   data
olle.blutti/sound_exit:    data
olle.blutti/sound_jump:    data
olle.blutti/sound_powerup: data
olle.blutti/sound_theme:   data
olle.blutti/sound_wrong:   data
olle.blutti/spritesheet:   data
olle.blutti/title:         data
$ wasm-objdump -j Export -x olle.blutti/_bin 

_bin:	file format wasm 0x1

Section Details:

Export[8]:
 - memory[0] -> "memory"
 - func[94] <cheat> -> "cheat"
 - func[95] <handle_menu> -> "handle_menu"
 - func[96] <boot> -> "boot"
 - func[98] <update> -> "update"
 - func[99] <render> -> "render"
 - global[1] -> "__data_end"
 - global[2] -> "__heap_base"
$ cat olle.blutti/level1 | jq -c
{"background_color":"LightBlue","font_color":"Black","particle_chance":5,"particle_sprite":88,"stars":10,"start_position":{"x":8,"y":144},"monsters":[{"position":{"x":144,"y":144},"sprite":128,"movement":-1}],"tiles":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,11,0,0,48,0,0,0,0,11,0,0,11,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,11,0,0,0,0,0,0,11,0,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,31,0,36,0,0,0,0,31,0,0,0,5,4,4,6,0,0,0,0,0,0,5,4,4,6,0,0,0,5,4,4,4,6,10,5,4,4,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,10,0,0,11,0,0,0,0,0,0,0,32,0,13,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,5,4,4,4,6,0,0,0,48,0,0,0,0,0,0,0,0,0,0,10,31,0,0,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,5,4,4,4,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,11,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,32,0,5,4,4,4,6,0,0,36,0,31,0,0,0,0,0,31,0,36,0,10,0,32,0,12,0,0,4,4,4,4,33,9,9,9,34,4,4,4,4,4,16,16,16,4,4,4,4,4,4,4,4,4,4,4,4,4]}

@peterhellberg
Copy link
Author

peterhellberg commented Feb 5, 2025

Snek

$ wget https://github.com/firefly-zero/snek/releases/latest/download/lux.snek.zip
$ unzipzstd lux.snek.zip
$ file lux.snek/*
lux.snek/_badges: Windows Precompiled iNF, version 1.3, InfStyle 1, flags 0x69206d09, unicoded, volatile dir ids, at 0x72756f79 "", at 0x70612030 WinDirPath, LanguageID 0
lux.snek/_bin:    WebAssembly (wasm) binary module version 0x1 (MVP)
lux.snek/_hash:   data
lux.snek/_meta:   data
lux.snek/font:    data
$ wasm-objdump -j Export -x lux.snek/_bin

_bin:	file format wasm 0x1

Section Details:

Export[8]:
 - memory[0] -> "memory"
 - func[13] <boot> -> "boot"
 - func[16] <update> -> "update"
 - func[20] <render> -> "render"
 - func[10] <before_exit> -> "before_exit"
 - func[24] <render_line> -> "render_line"
 - func[25] <cheat> -> "cheat"
 - func[26] <_initialize> -> "_initialize"

@peterhellberg
Copy link
Author

Tip

Note that you can unpack a Firefly ROM using the firefly_cli

$ firefly_cli import lux.snek.zip 
new device name: goofy-ruff1es
⚠️  verification failed: read key from ROM
✅ installed: /home/peter/.local/share/firefly/roms/lux/snek

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment