|
| 1 | +--[[ |
| 2 | +$module Functions for accessing Enigma XML files. |
| 3 | +
|
| 4 | +Enigma XML is the underlying file format of a Finale `.musx` file. It is undocumented |
| 5 | +by MakeMusic and must be extracted from the `.musx` file. There is an effort to document |
| 6 | +it underway at the [EnigmaXML Documentation](https://github.com/finale-lua/enigmaxml-documentation) |
| 7 | +repository. |
| 8 | +]] -- |
| 9 | +local enigmaxml = {} |
| 10 | + |
| 11 | +local utils = require("library.utils") |
| 12 | + |
| 13 | +-- symmetrical encryption/decryption function |
| 14 | +local function crypt_enigmaxml_buffer(buffer) |
| 15 | + local state = 0x28006D45 -- this value was determined empirically |
| 16 | + local result = {} |
| 17 | + |
| 18 | + for i = 1, #buffer do |
| 19 | + -- BSD rand() |
| 20 | + state = (state * 0x41c64e6d + 0x3039) & 0xFFFFFFFF -- Simulate 32-bit overflow |
| 21 | + local upper = state >> 16 |
| 22 | + local c = upper + math.floor(upper / 255) |
| 23 | + |
| 24 | + local byte = string.byte(buffer, i) |
| 25 | + byte = byte ~ (c & 0xFF) -- XOR operation on the byte |
| 26 | + |
| 27 | + table.insert(result, string.char(byte)) |
| 28 | + end |
| 29 | + |
| 30 | + return table.concat(result) |
| 31 | +end |
| 32 | + |
| 33 | +--[[ |
| 34 | +%extract_xml |
| 35 | +
|
| 36 | +Extracts an enigmaxml buffer from a `.musx` file. Note that this function does not work with Finale's |
| 37 | +older `.mus` format. |
| 38 | +
|
| 39 | +Windows users must have `7z` installed and macOS users must have `unzip`. |
| 40 | +
|
| 41 | +@ filepath (string) a file path to a `.musx` file. |
| 42 | +: (string) buffer of xml data containing the EnigmaXml extracted from the `.musx` |
| 43 | +]] |
| 44 | +function enigmaxml.extract_xml(filepath) |
| 45 | + if finenv.MajorVersion <= 0 and finenv.MinorVersion < 68 then |
| 46 | + error("enigmaxml.extract_xml requires at least RGP Lua v0.68.", 2) |
| 47 | + end |
| 48 | + if finenv.TrustedMode == finenv.TrustedModeType.UNTRUSTED then |
| 49 | + error("enigmaxml.extract_xml must run in Trusted mode.", 2) |
| 50 | + end |
| 51 | + local _, _, extension = utils.split_file_path(filepath) |
| 52 | + if extension ~= ".musx" then |
| 53 | + error(filepath .. " is not a .musx file.", 2) |
| 54 | + end |
| 55 | + |
| 56 | + local text = require("luaosutils").text |
| 57 | + local process = require("luaosutils").process |
| 58 | + |
| 59 | + local os_filepath = text.convert_encoding(filepath, text.get_utf8_codepage(), text.get_default_codepage()) |
| 60 | + local output_dir = os.tmpname() |
| 61 | + local rmcommand = (finenv.UI():IsOnMac() and "rm " or "cmd /c del ") .. output_dir |
| 62 | + process.execute(rmcommand) |
| 63 | + |
| 64 | + local zipcommand |
| 65 | + if finenv.UI():IsOnMac() then |
| 66 | + zipcommand = "unzip \"" .. os_filepath .. "\" -d " .. output_dir |
| 67 | + else |
| 68 | + zipcommand = "cmd /c 7z x -o" .. output_dir .. " \"" .. os_filepath .. "\"" |
| 69 | + end |
| 70 | + if not process.execute(zipcommand) then |
| 71 | + error(zipcommand .. " failed") |
| 72 | + end |
| 73 | + |
| 74 | + local file <close> = io.open(output_dir .. "/score.dat", "rb") |
| 75 | + if not file then |
| 76 | + error("unable to read " .. output_dir .. "/score.dat") |
| 77 | + end |
| 78 | + local buffer = file:read("*all") |
| 79 | + file:close() |
| 80 | + |
| 81 | + local delcommand = (finenv.UI():IsOnMac() and "rm -r " or "cmd /c rmdir /s /q ") .. output_dir |
| 82 | + process.execute(delcommand) |
| 83 | + |
| 84 | + return crypt_enigmaxml_buffer(buffer) |
| 85 | +end |
| 86 | + |
| 87 | +return enigmaxml |
0 commit comments