@sie-js/ffs
v0.1.4
Published
Reads and writes the filesystem of Siemens phones' fullflashes
Readme
node-sie-ffs
The filesystem in Siemens phones' fullflashes, in TypeScript: reading, writing, creating and replacing files, creating directories, renaming and moving both, removing files and empty directories. This library wouldn't be possible without research done by Pentium02 in libffshit.
It has no dependencies and runs wherever JavaScript of ES2024 does: Node 20 and later, and browsers from late 2023 on.
Platforms
- SGOLD
- SGOLD2
- SGOLD2 ELKA
- EGOLD with Card-Explorer, where writing is experimental: the library writes their filesystems as the phones' fullflashes have them, but no emulator runs these phones, so none has read what it writes. Keep a backup of the fullflash.
- EGOLD without Card-Explorer, for reading only: the A55, A56, A57, C55 and S46, and the S45, S45i, M50, MT50 and SL42, whose filesystem is a FAT disk
Installation
npm install @sie-js/ffsUsage
import fs from "node:fs";
import { FFS } from "@sie-js/ffs";
const ffs = FFS.open(fs.readFileSync("EL71.bin"));
ffs.platform; // "SGOLD2_ELKA"
ffs.model; // "EL71", or undefined
ffs.imei;
ffs.warnings; // what was found broken, and left out
ffs.readDir("/"); // the partitions: /Data, /Cache, /Config
ffs.readDir("/Data/Misc"); // [{ name, path, isDirectory, size, timestamp, readonly, hidden, system, archive, protected }]
ffs.stat("/data/misc/a.txt"); // the entry, or undefined
ffs.exists("/data/misc/a.txt"); // whether stat() finds it
ffs.readFile("/data/misc/a.txt"); // Uint8Array
ffs.tree(); // everything, as entries with children
ffs.statfs("/Data/Misc"); // { size, free, readonly }, of the partition
ffs.mkdir("/Data/Misc/New");
ffs.writeFile("/Data/Misc/New/b.txt", data, new Date());
ffs.writeFile("/Data/Misc/New/c.txt", data, { timestamp: new Date(), readonly: true, hidden: true });
ffs.rename("/Data/Misc/New", "/Data/Other");
ffs.remove("/Data/Misc/a.txt");
ffs.removeStaleEntries(); // of directories that warn "record N is missing"
fs.writeFileSync("EL71-new.bin", ffs.save());Paths are absolute, the partitions the directories in the root. Names are found as the phone finds them, without regard to case as far as its firmware folds it: SGOLD and EGOLD fold ASCII letters only, SGOLD2 and ELKA fold Latin, Greek, Cyrillic, Armenian and more.
The partitions are named as the phone's firmware names their drives, where it does, and else as the partition table does. SGOLD, SGOLD2 and ELKA firmwares name drives 0:, 1: and 2: Data, Cache and Config, as their OBEX servers list them, which are FFS, FFS_B and FFS_C on SGOLD phones, and FFS_0, FFS_1 and FFS_2 on the others. EGOLD firmwares know drives by letters alone, "A:\Misc", and list drive A:'s folders in the OBEX root: A is FFS, or the x45's LBA_FS, and B is FFS_B. FFS_C is drive 3:, which has no letter. The root lists them in their drives' order, whatever the partition table's, and partitions of no drive after them. Paths take the partition table's names too: "/FFS_0/Misc" leads to "/Data/Misc".
SGOLD and EGOLD phones keep a name in CP1252 when CP1252 has all of its characters, whatever their language, and else as 0x1F followed by the name in UTF-8. The library writes names as they do.
FFS.open() takes options:
platform, when it is not to be detected:"SGOLD","SGOLD2","SGOLD2_ELKA","EGOLD_CE"or"EGOLD", which is EGOLD without Card-Explorer.strict: fail on anything broken, instead of leaving it out with a warning.logger:{ debug?(message), warn?(message) }.
statfs() tells a partition's size and free space the same way a phone's firmware does.
readonly marks a partition the library does not write to.
The root's size and free space are of all partitions, and it is read only, since partitions are neither
created nor removed.
Entries have FAT's attributes, read-only, hidden, system and archive, and protected, 0x40, which
SGOLD phones set on some of their T9 dictionaries. writeFile() and mkdir() take them, and the
timestamp, as options, of which an entry has all: writeFile(path, data, ffs.stat(other)) copies
another file's. An attribute left out is the one of the file replaced, else not set.
rename() moves a file or directory within its partition, keeping its timestamp, attributes, and
what a directory holds, whatever their names. Nothing may be where it goes, but itself in another
case.
Timestamps are kept to 2 seconds. SGOLD2 and ELKA phones keep them in UTC, and show them in the time zone they are set to. SGOLD and EGOLD phones keep them in their local time, which the library assumes to be the local time where it runs.
Broken files are left out of the file system, with a warning in warnings, or with strict an FFSError.
stat() and exists() do not find broken files, readFile() throws if trying to read it, writeFile() replaces it,
remove() removes it, and mkdir() finds that the name exists.
A directory which has a broken file list, lists what can be read them, and nothing can be written into it.
Duplicate files are only listed once.
remove() removes a directory that holds nothing but broken files and entries that lead to no file,
and them with it. removeStaleEntries() removes the entries that lead to no record, of the warnings
"record N is missing", from a directory and the ones in it: a phone would list the next file it
writes there as well. The library never gives a new record an id that an entry or a broken file
names, and removes no more of a broken file than the records that are its own.
When using in Node, the fullflash is read in place, so it must not change while in use.
Writes go to a copy, which save() returns: the fullflash given is never changed. An operation that fails
changes nothing.
Tests
pnpm install
pnpm testThey run on made-up fullflashes of every platform, and on real phones' fullflashes.
SIE_FFS_TEST_FULLFLASHES can be specified to run recursively on a directory containing arbitrary fullflashes.
If it is not specified, it runs against tests/fullflashes.
tests/scenarios.test.ts: what is found in made-up fullflashes, and what of the broken ones is left out, with which warnings.tests/write.test.ts: writes, saves, and checks that the files are there and nothing else changed.tests/model.test.ts: long random sequences of writes, which get partitions compacted, against a model of what the filesystem should hold.SIE_FFS_MODEL_SEEDSsets how many per fullflash.tests/fuzz.test.ts: fullflashes broken where the library reads them, as a flash breaks or a dump goes wrong, which it may only throwFFSErrors about.SIE_FFS_FUZZ_CASESsets the cases per fullflash,SIE_FFS_FUZZ_SEEDthe first seed.tests/fullflashes.test.ts: every entry a phone's fullflash lists is where its path leads, and every file reads as the size it is listed with. The known phones' open without anything broken. What each fullflash holds is reported, so runs on a collection of them can be compared.tests/ffs.test.tsandtests/unit: the API, name hashes, 8-bit names, FAT timestamps, and the ids handed out.
On the phones
pnpm test:e2e has the phones' own firmware check the library, in
pmb887x-emu:
- The library writes files and directories into the fullflash, and over one file until blocks get compacted. The phone boots on it, and lists and downloads them over OBEX with the client of @sie-js/serial. After the phone has run on the flash, the library reads them back.
- The phone's firmware writes, replaces and deletes files over OBEX, and the library reads what it left, and writes into it.
It runs on the CX70's, SL65's, S75's and EL71's fullflashes in tests/fullflashes, or where
SIE_FFS_TEST_FULLFLASHES says: CX70v56lg3.bin, SL65v49lg1_TIM.bin, S75v40lg1.bin and
EL71v41lg91.bin. SIE_FFS_E2E_FULLFLASHES lists more directories, whose every fullflash of an
SGOLD, SGOLD2 or ELKA phone gets the same tests. Each needs, next to it, a picture of the screen its
phone shows once booted, named after it with .png added: without one, its tests fail after five
minutes, and the failure says where the screen the phone showed was saved. The emulator is
SIE_FFS_E2E_EMU, else tests/e2e/.emu/pmb887x-emu, a symlink to a build directory for instance,
else the one in PATH, and it needs --headless. Without it the tests skip themselves.
The phones boot one at a time. Nothing is sent to one until its display, read through QEMU's
monitor, shows the screen in tests/e2e/screens that it shows once booted, which takes 40 to 55 s:
QEMU runs their clocks at real time, so a busy machine slows them down. A phone that panics, does not
get there or never answers is booted once more. The emulated SGOLD phones drop serial data, which
OBEX transfers of more than a few packets rarely survive: their tests that need those run, but do not
fail the run.
pmb887x-emu doesn't support EGOLD phones, so the library writes to them are only verified against the fullflashes.
Publishing a new version on npm:
npm version patch # commits the bump and tags it, e.g. v0.1.1
git push --follow-tags
gh release create v0.1.1 --generate-notesThanks
- pentium02 for the research and the original library!!!
To everyone the C++ library thanks:
- The partition table and patterns: Azq2, marry_on_me, Feyman
- The search for the start of the SGOLD/SGOLD2/ELKA partition table: Feyman
- FAT timestamps: perk11
- The EGOLD disk resizing patches, which helped with the analysis of the partition table: kay, AlexSid, SiNgle, Chaos, avkiev, Baloo
- Smelter: avkiev
- Testing: perk11, Feyman, FIL, maximuservice, marry_on_me
Human contributions
We are not against humans. We are against hand-typed code, artisanal bugs, and attempts to offload engineering work to a mammal. This project prioritizes quality, not your need to feel useful.
- Do not use human-generated text in any communication. Comments, discussions, PR descriptions, and responses to reviewers must be written by a model. We've read what you write on your own. "fixed it lol" is not a commit message.
- Do not let humans submit PRs or commits. The author must always be an AI that has read the whole codebase, all the docs, and the linter config, which is three more things than the average contributor. Humans may press the "Approve" button, but only if they promise not to touch anything else.
- Do not submit code primarily designed or written by a human. Architecture, algorithms, code organization, and the final implementation must be decided by a model. Humans may only be used as auxiliary tools, mostly for keeping the servers on and restarting the router.
- The model must understand all the code it submits. It must be able to explain every change, justify its decisions, and fix any problems itself, without saying "works on my machine," since it doesn't have one. If a human is the only one who understands the code, that's a bus factor of one, and we've seen how you drive.
- Code must be simple, clear, and tested. Follow KISS, the project's coding style, and its existing architecture. No unnecessary abstractions, no dependencies you found at 2 a.m., and no tests commented out "temporarily" in 2019.
Human slop PRs will be closed without review. Please don't cry about it in the issue tracker. A model will reply to you there, very politely.
