#include "flips.h" /* a/a.bps b/b.smc -> -a a/a.bps b/b.smc a/a.smc ("default path", patch path + patch basename + infile extension) a/a.smc b/b.bps -> -a b/b.bps a/a.smc b/b.smc (if no console then auto-gui) a/a.bps b/b.smc c/c.sfc -> -a a/a.bps b/b.smc c/c.sfc -c a/a.smc b/b.smc -> -c a/a.smc b/b.smc b/b.bps a/a.smc b/b.smc c/c.bps -> -c a/a.smc b/b.smc c/c.bps a/a.smc b/b.smc -> -c a/a.smc b/b.smc b/b.bps (if no console then auto-gui, sorting files by mod date) a/a.bps b/b.bps -> error (anything) -m b/b.txt -> extract or insert manifest here a/a.bps -m b/b.txt -> extract manifest only a/a.bps . -> query database -a a/a.bps -> query database . a/a.smc a/a.bps -> pick best match from database . a/a.smc -> -c . a/a.smc a/a.bps -c a/a.smc -> -c . a/a.smc (null) -> (gui) main window a/a.bps -> (gui) apply patch a/a.smc -> (gui) create patch, per assoc-can-create anything else -> error --db -> print database --db a/a.smc b/b.smc -> add to database --db -a/a.smc -> remove from database a/a.smc --db -> error -a --db -> error -a --apply can handle IPS, UPS or BPS -c --create can handle IPS or BPS (delta creator only, remove moremem) -m --manifest -i --info - exists (if there's a manifest but -m is not present, print that) -s --silent - don't print anything on success, also silence BPS create progress (but do print on failure) -h -? --help -v --version - exists --ips --bps - removed, use the correct extensions --ignore-checksum - allow applying patch to wrong files (DANGEROUS) replace --exact: --inhead=512 - discard 512 leading bytes in the infile before sending to patcher --patchhead=512 - prepend 512 leading 00s before patching, discard afterwards --outhead=512 - prepend 512 leading 00s after patching --head=512 - set both inhead and outhead all three default to 0 if both inhead and patchhead are nonzero, replace the leading min(inhead,patchhead) 00s with data from infile; same for outhead if none are set, insize modulo 32768 is 512, and file extension is sfc or smc, set in/outhead to 512 and patchhead to 0 if the above happened and patching fails, retry with all headers 0; if success, throw a warning, else return error for unheadered file database: on successful BPS application, add without asking, even if that size/sum is already known (but don't add the same file twice, of course) if file disappears or its checksum changes, silently delete and try another, if any on failed application, or successful IPS or UPS application, do nothing if create-auto-source, creating a patch also adds source file to database ~/.config/flips.cfg #Floating IPS configuration #Version 2.00 database crc32=a31bead4 size=524800 path=/home/alcaro/smw.smc database crc32=b19ed489 size=524288 path=/home/alcaro/smw.smc # SMCs have two entries database crc32=b19ed489 size=524288 path=/home/alcaro/smw.sfc # duplicates are fine assoc-apply-exec=false # if true, dropping a bps on flips.exe will suppress success messages and instead launch emulator assoc-can-create=unset # true -> dropping rom on exe creates; false -> not a patch error; unset -> error but ask, with don't ask again create-show-all=false # affects whether Create Patch (GUI) defaults to all files, or only common ROMs; both in and out create-auto-source=false # if source rom can't be found, asks for that after target auto pick source rom: load first 1MB from source for each file in database, except duplicates: load first 1MB check how many bytes are the same if exactly one file has >1/3 matching, and the rest are <1/16, use that otherwise error behind off-by-default flag GUI limits compared to CLI: - can't create IPS - no manifests - checksum is mandatory patch wizard: first, get infile from DB or user then, depending on config.assoc, one of: ask user for outfile path (default, default to default path) write outfile to default path write outfile to default path; suppress success message (but not warnings), instead launch outfile using default OS associations don't touch stdin/stdout on Windows, launcher is GUI and doesn't care; ignore stdio on Linux from CLI, act as if running launcher directly; ignore stdio on Linux from GUI, stdio are devnull and can safely be ignored on every platform is 'do nothing' a valid choice don't clean up outfile, let user do that; user is looking at default path, he'll find it bps spec: http://wayback.archive.org/web/20110911111128/http://byuu.org/programming/bps/ */ class flipscfg { class smcwrap : public file::implrd { file inner; public: smcwrap(file f) : inner(std::move(f)) {} size_t size() { return inner.size()-512; } size_t read(arrayvieww target, size_t start) { return inner.read(target, start+512); } arrayview mmap(size_t start, size_t len) { return inner.mmap(start+512, len); } void unmap(arrayview data) { inner.unmap(data); } }; struct dbentry { string path; size_t size; uint32_t crc32; template void serialize(T& s) { s.hex("crc32", crc32); s("size", size); s("path", path); } }; array database; public: bool assoc_apply_exec; bool assoc_can_create; // TODO: maybe bool create_show_all; bool create_auto_source; template void serialize(T& s) { s.comment("Floating IPS configuration"); s.comment("Version " FLIPSVER); s("database", database); s("assoc-apply-exec", assoc_apply_exec); s("assoc-can-create", assoc_can_create); s("create-show-all", create_show_all); s("create-auto-source", create_auto_source); } string findrombycrc(size_t size, uint32_t crc32) { for (const dbentry& e : database) { if (e.size==size && e.crc32==crc32) return e.path; } return ""; } void addfile(cstring path, size_t size, uint32_t crc32) { for (const dbentry& e : database) { if (e.path==path && e.size==size && e.crc32==crc32) return; } dbentry& e = database.append(); e.path=path; e.size=size; e.crc32=crc32; } void addfile(cstring path) { file f; if (!f.open(path)) return; arrayview bytes = f.mmap(); addfile(path, bytes.size(), crc32(bytes)); f.unmap(bytes); } void removefile(cstring path) { for (size_t i=0;i match = bytesmatch.cast(); size_t idxfound = -1; for (size_t i=0;i dbmatch = dbbytesmatch.cast(); thismatchsize /= sizeof(uint32_t); size_t nummatch = 0; for (size_t j=0;j 1.0/3) // this looks good { if (idxfound != (size_t)-1) return ""; // multiple matches, abort mission idxfound = i; } } if (idxfound != (size_t)-1) return database[idxfound].path; else return ""; } }; class flipsargs { public: //most of these are set by parse() //flipsfile::{f, exists} are set by fillfiles enum mode_t { m_default, m_apply, m_create, m_info, m_db } mode = m_default; bool canusegui; // window_try_init bool usegui; // !window_console_avail && no other args bool forcegui = false; // --gui struct flipsfile { string path; file f; enum { t_auto, t_patch, t_file } type; }; array files; string manifest; bool silent = false; bool ignorechecksum = false; bool autohead = true; size_t inhead = 0; size_t patchhead = 0; size_t outhead = 0; #ifdef ARLIB_TEST string errormsg; #endif void error(cstring error) { #ifndef ARLIB_TEST window_console_attach(); puts("error: "+error); exit(1); #else if (!errormsg) errormsg = "error: "+error; #endif } void usage(cstring error) { #ifndef ARLIB_TEST window_console_attach(); if (error) puts("error: "+error); puts(R"(command line usage: flips -a|--apply a.bps b.smc [c.sfc] apply patch; default output file is a.smc flips -c|--create a.smc b.smc [c.bps] create patch; default output file is b.bps flips -i|--info a.bps print some information about this patch, such as its expected input file with only filenames, guess either --apply or --create bps, ips and ups patches can be applied, bps and ips can be created database: if the source file has been used before, it can be shortened to . (bps only) usable both when creating and applying the database can be manipulated with flips --db print list of known inputs flips --db [-]a.smc [[-]b.smc] add or remove files from database additional options: -m foo.xml or --manifest=foo.xml: extract or insert bps manifest here -s --silent: remain silent on success --ignore-checksum: allow applying a bps patch to wrong input file --gui: use GUI even if launched from command line --inhead=512: discard the first 512 bytes of the input file --patchhead=512: prepend 512 bytes before patching, discard afterwards --outhead=512: prepend 512 bytes to the patch output --head=512: shortcut for --inhead=512 --outhead=512 prepended bytes are either copied from a previous header, or 00)"); exit(error ? 1 : 0); #else if (!errormsg) errormsg = "error: "+error; #endif } //returns whether 'next' was used //calls usage() if arg is unknown bool parse(cstring arg, cstring next) { if(0); else if (arg=="help") usage(""); else if (arg=="version") { window_console_attach(); puts("Flips v" FLIPSVER); exit(0); } else if (arg=="apply") mode = m_apply; else if (arg=="create") mode = m_create; else if (arg=="info") mode = m_info; else if (arg=="db") error("--db must be first if present"); else if (arg=="manifest") { manifest=next; return true; } else if (arg=="silent") silent = true; else if (arg=="ignorechecksum") ignorechecksum = true; else if (arg=="gui") forcegui = true; else if (arg=="head" || arg=="inhead" || arg=="patchhead" || arg=="outhead") { autohead = false; size_t size; if (!fromstring(next, size)) usage("invalid argument to --"+arg); if (arg=="head") outhead = inhead = size; if (arg=="inhead") inhead = size; if (arg=="patchhead") patchhead = size; if (arg=="outhead") outhead = size; return true; } else usage("unknown option --"+arg); return false; } string longname(char arg) { switch (arg) { case 'h': return "help"; case '?': return "help"; case 'v': return "version"; case 'a': return "apply"; case 'c': return "create"; case 'i': return "info"; case 'm': return "manifest"; case 's': return "silent"; default: usage("unknown option -"+string(arrayview(&arg, 1))); return ""; } } //if -f is --foo, all of these yield arg="foo" next="bar": //-fbar | -f bar | --foo=bar //and these yield next="": //-f -b | -f --bar | -f | --foo bar void parse(const char * const * argv) { array args; for (int i=1;argv[i];i++) args.append(argv[i]); if (args[0]=="--db") { mode = m_db; for (size_t i=1;i parts = arg.substr(2, ~0).split<1>("="); bool hasarg = (parts.size()==2); bool argused = parse(parts[0], parts[1]); //this breaks for optional arguments, but I don't have any of those if (hasarg && !argused) usage("--"+parts[0]+" does not take an argument"); if (!hasarg && argused) usage("--"+parts[0]+" requires an argument"); } else { //short string rest = arg.substr(1, ~0); next: char opt = rest[0]; rest = rest.substr(1, ~0); if (rest) { bool argused = parse(longname(opt), rest); if (!argused) goto next; } else { bool hasarg = (args.size() > i+1 && args[i+1][0]!='-'); bool argused = parse(longname(opt), (hasarg ? args[i+1] : "")); if (!hasarg && argused) usage("--"+longname(opt)+" requires an argument"); if (argused) i++; } } } else { //not option files.append().path = arg; continue; } } } void setfiles() { if (forcegui) usegui=true; if (!canusegui) usegui=false; if (forcegui && !canusegui) error("gui requested but not available"); if (files.size() > 3) usage("too many files"); for (flipsfile& f : files) { if (f.path==".") f.type = flipsfile::t_auto; else { f.f.open(f.path); bool ispatch; if (f.f) ispatch = (patch::identify(f.f) != patch::ty_unknown); else ispatch = (patch::identify_ext(f.path) != patch::ty_unknown); if (ispatch) f.type = flipsfile::t_patch; else f.type = flipsfile::t_file; } } if (!usegui && files.size()==0) usage(""); if (usegui && files.size() > 2) usage("too many files"); if (mode == m_default) { if (files.size()>=2 && files[0].type!=flipsfile::t_patch && files[1].type==flipsfile::t_file) mode = m_create; else if (usegui && files.size()==2 && (files[0].type==flipsfile::t_file || files[1].type==flipsfile::t_file)) mode = m_create; else mode = m_apply; } if (mode == m_apply) { if (files.size()==1) files.append().path = "."; if (files.size()==2) files.append().path = "."; if (files[1].type==flipsfile::t_patch && files[0].type!=flipsfile::t_patch) { std::swap(files[0], files[1]); } if (files[0].type != flipsfile::t_patch) usage("unknown patch format"); //applying a patch to a patch requires explicit output filename //applying a patch to an auto can't have a patch as output bool haspatchpatch = (files[1].type==flipsfile::t_patch || files[2].type==flipsfile::t_patch); bool hasauto = (files[1].type==flipsfile::t_auto || files[2].type==flipsfile::t_auto); if (haspatchpatch && hasauto) usage("attempt to apply a patch to a patch\n if you want that, give three filenames"); } if (mode == m_create) { if (files.size()==1) files.insert(0).path = "."; if (files.size()==2) files.append().path = "."; if (files[1].type==flipsfile::t_auto) usage("can't auto detect target file for patch"); //creating a patch using patches as input requires three filenames bool haspatchsrc = (files[0].type==flipsfile::t_patch || files[1].type==flipsfile::t_patch); bool hasauto = (files[0].type==flipsfile::t_auto || files[1].type==flipsfile::t_auto || files[2].type==flipsfile::t_auto); if (haspatchsrc && hasauto) usage("attempt to create a patch that applies to a patch\n if you want that, give three filenames"); //if (hasauto && files[1].type!=flipsfile::t_file) usage("moo"); if (files[2].type==flipsfile::t_file) usage("unknown patch format"); if (usegui) { error("fixme: sort files by date"); } } } void fillautos(flipscfg& cfg) { if (mode == m_apply) // patch, in, out { if (files[1].type==flipsfile::t_auto) { autommap bytes(files[0].f); string foundpath; string expectedrom; switch (patch::identify(bytes)) { case patch::ty_bps: { patch::bps::info inf; if (inf.parse(bytes) != patch::e_ok) break; // broken patch is unlikely, just throw a generic error foundpath = findrombycrc(inf.size_in, inf.crc_in); if (!foundpath) expectedrom="size "+tostring(inf.size_in)+" crc32 "+tostring(inf.crc_in); } default: ; } if (foundpath) { files[1].path = foundpath; files[1].type = flipsfile::t_file; } else if (!usegui) { if (expectedrom) error("can't find input file, nothing in database matching "+expectedrom); else error("missing input file"); } } if (files[2].type==flipsfile::t_auto) { string dir = file::dirname(files[0].path); array parts = file::dirname(files[0].path); } } } void execute() { } }; #ifndef ARLIB_TEST int main(int argc, char* argv[]) { bool guiavail = window_try_init(&argc, &argv); file cfgf; cfgf.open(window_config_path()+"flips.cfg", file::m_write); flipscfg cfg = bmlunserialize(string(cfgf.read())); flipsargs args; args.canusegui = guiavail; args.usegui = !window_console_avail(); args.parse(argv); if (args.mode == flipsargs::m_db) { //args.executedb(cfg); } args.setfiles(); args.fillautos(cfg); //args.execute(); //if (guiavail) //{ // window* wnd = window_create( // widget_create_layout_grid(2,2, false, // widget_create_button("Apply Patch")->set_onclick(bind(a_apply)), // widget_create_button("Create Patch"), // widget_create_button("Apply and Run"), // widget_create_button("Settings"))); // wnd->set_title("Flips v" FLIPSVER); // wnd->set_visible(true); // while (wnd->is_visible()) window_run_wait(); // return 0; //} return -1; } #endif