forked from finale-lua/lua-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
225 additions
and
87 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
--[[ | ||
$module ziputils | ||
Functions for unzipping files. (Future may include zipping as well.) | ||
Dependencies: | ||
- Windows users must have `7z` installed. You can download it [here](https://www.7-zip.org/). | ||
- MacOS users must have `unzip` and `gunzip`, but these are usually installed with the OS. | ||
Pay careful attention to the comments about how strings are encoded. They are either encoded | ||
**platform** or **utf-8**. On macOS, platform encoding is always utf-8, but on Windows it can | ||
be any number of encodings depending on the locale settings and version of Windows. You can use | ||
`luaosutils.text` to convert them back and forth. Both `luaosutils.process.execute` | ||
requires platform encoding as do `lfs` and all built-in Lua `io` functions. | ||
Note that many functions require later versions of RGP Lua that include `luaosutils` | ||
and/or `lfs`. But the these dependencies are embedded in each function so that any version | ||
of Lua for Finale can at least load the library. | ||
]] -- | ||
local ziputils = {} | ||
|
||
local utils = require("library.utils") | ||
|
||
-- This variable allows us to check if we are supported when we load and the functions | ||
-- can throw out based on it. | ||
local not_supported_message | ||
if finenv.MajorVersion <= 0 and finenv.MinorVersion < 68 then | ||
not_supported_message = "ziputils requires at least RGP Lua v0.68." | ||
elseif finenv.TrustedMode == finenv.TrustedModeType.UNTRUSTED then | ||
not_supported_message = "ziputils must run in Trusted mode." | ||
elseif not finaleplugin.ExecuteExternalCode then | ||
not_supported_message = "ziputils.extract_enigmaxml must have finaleplugin.ExecuteExternalCode set to true." | ||
end | ||
|
||
--[[ | ||
% calc_rmdir_command | ||
Returns the platform-dependent command to remove a directory. It can be passed | ||
to `luaosutils.process.execute`. | ||
**WARNING** The command, if executed, permanently deletes the contents of the directory. | ||
You would normally call this on the temporary directory name from `calc_temp_output_path`. | ||
But it works on any directory. | ||
@ path_to_remove (string) platform-encoded path of directory to remove. | ||
: (string) platform-encoded command string to execute. | ||
]] | ||
function ziputils.calc_rmdir_command(path_to_remove) | ||
return (finenv.UI():IsOnMac() and "rm -r " or "cmd /c rmdir /s /q ") .. path_to_remove | ||
end | ||
|
||
--[[ | ||
% calc_delete_file_command | ||
Returns the platform-dependent command to delete a file. It can be passed | ||
to `luaosutils.process.execute`. | ||
**WARNING** The command, if executed, permanently deletes the file. | ||
You would normally call this on the temporary directory name from `calc_temp_output_path`. | ||
But it works on any directory. | ||
@ path_to_remove (string) platform-encoded path of directory to remove. | ||
: (string) platform-encoded command string to execute. | ||
]] | ||
function ziputils.calc_delete_file_command(path_to_remove) | ||
return (finenv.UI():IsOnMac() and "rm " or "cmd /c del ") .. path_to_remove | ||
end | ||
|
||
|
||
--[[ | ||
% calc_temp_output_path | ||
Returns a path that can be used as a temporary target for unzipping. The caller may create it | ||
either as a file or a directory, because it is guaranteed not to exist when it is returned and it does | ||
not have a terminating path delimiter. Also returns a platform-dependent unzip command that can be | ||
passed to `luaosutils.process.execute` to unzip the input archive into the temporary name as a directory. | ||
The command may not be compatible with `os.execute`. | ||
This function requires `luaosutils`. | ||
@ [archive_path] (string) platform-encoded filepath to the zip archive that is included in the zip command. | ||
: (string) platform-encoded temporary path generated by the system. | ||
: (string) platform-encoded unzip command that can be used to unzip a multifile archived directory structure into the temporary path. | ||
]] | ||
function ziputils.calc_temp_output_path(archive_path) | ||
if not_supported_message then | ||
error(not_supported_message, 2) | ||
end | ||
|
||
archive_path = archive_path or "" | ||
|
||
local process = require("luaosutils").process | ||
|
||
local output_dir = os.tmpname() | ||
local rmcommand = ziputils.calc_delete_file_command(output_dir) | ||
process.execute(rmcommand) | ||
|
||
local zipcommand | ||
if finenv.UI():IsOnMac() then | ||
zipcommand = "unzip \"" .. archive_path .. "\" -d " .. output_dir | ||
else | ||
zipcommand = "cmd /c 7z x -o" .. output_dir .. " \"" .. archive_path .. "\"" | ||
end | ||
return output_dir, zipcommand | ||
end | ||
|
||
--[[ | ||
% calc_gunzip_command | ||
Returns the platform-dependent command to gunzip a file. It can be passed | ||
to `luaosutils.process.execute`, which will then return the text directly. | ||
@ archive_path (string) platform-encoded path of source gzip archive. | ||
: (string) platform-encoded command string to execute. | ||
]] | ||
function ziputils.calc_gunzip_command(archive_path) | ||
if finenv.UI():IsOnMac() then | ||
return "gunzip -c " .. archive_path | ||
else | ||
return "7z e -so " .. archive_path | ||
end | ||
end | ||
|
||
--[[ | ||
% calc_is_gzip | ||
Detects if an input buffer is a gzip archive. Sometimes, Finale gzips the internal EnigmaXML document. | ||
@ buffer (string) binary data to check if it is a gzip archive | ||
: (boolean) true if the buffer is a gzip archive | ||
]] | ||
function ziputils.calc_is_gzip(buffer) | ||
local byte1, byte2, byte3, byte4 = string.byte(buffer, 1, 4) | ||
return byte1 == 0x1F and byte2 == 0x8B and byte3 == 0x08 and byte4 == 0x00 | ||
end | ||
|
||
-- symmetrical encryption/decryption function for EnigmaXML | ||
local function crypt_enigmaxml_buffer(buffer) | ||
local INITIAL_STATE <const> = 0x28006D45 -- this value was determined empirically | ||
local state = INITIAL_STATE | ||
local result = {} | ||
|
||
for i = 1, #buffer do | ||
-- BSD rand() | ||
if (i - 1) % 0x20000 == 0 then | ||
state = INITIAL_STATE | ||
end | ||
state = (state * 0x41c64e6d + 0x3039) & 0xFFFFFFFF -- Simulate 32-bit overflow | ||
local upper = state >> 16 | ||
local c = upper + math.floor(upper / 255) | ||
|
||
local byte = string.byte(buffer, i) | ||
byte = byte ~ (c & 0xFF) -- XOR operation on the byte | ||
|
||
table.insert(result, string.char(byte)) | ||
end | ||
|
||
return table.concat(result) | ||
end | ||
|
||
--[[ | ||
%extract_enigmaxml | ||
EnigmaXML is the underlying file format of a Finale `.musx` file. It is undocumented | ||
by MakeMusic and must be extracted from the `.musx` file. There is an effort to document | ||
it underway at the [EnigmaXML Documentation](https://github.com/finale-lua/ziputils-documentation) | ||
repository. | ||
This function extracts the EnigmaXML buffer from a `.musx` file. Note that it does not work with Finale's | ||
older `.mus` format. | ||
@ filepath (string) utf8-encoded file path to a `.musx` file. | ||
: (string) utf8-encoded buffer of xml data containing the EnigmaXml extracted from the `.musx`. | ||
]] | ||
function ziputils.extract_enigmaxml(filepath) | ||
if not_supported_message then | ||
error(not_supported_message, 2) | ||
end | ||
local _, _, extension = utils.split_file_path(filepath) | ||
if extension ~= ".musx" then | ||
error(filepath .. " is not a .musx file.", 2) | ||
end | ||
|
||
local text = require("luaosutils").text | ||
local process = require("luaosutils").process | ||
|
||
local os_filepath = text.convert_encoding(filepath, text.get_utf8_codepage(), text.get_default_codepage()) | ||
local output_dir, zipcommand = ziputils.calc_temp_output_path(os_filepath) | ||
if not process.execute(zipcommand) then | ||
error(zipcommand .. " failed") | ||
end | ||
|
||
local file <close> = io.open(output_dir .. "/score.dat", "rb") | ||
if not file then | ||
error("unable to read " .. output_dir .. "/score.dat") | ||
end | ||
local buffer = file:read("*all") | ||
file:close() | ||
|
||
local delcommand = ziputils.calc_rmdir_command(output_dir) | ||
process.execute(delcommand) | ||
|
||
buffer = crypt_enigmaxml_buffer(buffer) | ||
if ziputils.calc_is_gzip(buffer) then | ||
local gzip_path = ziputils.calc_temp_output_path() | ||
local gzip_file <close> = io.open(gzip_path, "wb") | ||
if not gzip_file then | ||
error("unable to create " .. gzip_file) | ||
end | ||
gzip_file:write(buffer) | ||
gzip_file:close() | ||
local gunzip_command = ziputils.calc_gunzip_command(gzip_path) | ||
buffer = process.execute(gunzip_command) | ||
process.execute(ziputils.calc_delete_file_command(gzip_path)) | ||
if not buffer or buffer == "" then | ||
error(gunzip_command .. "failed") | ||
end | ||
end | ||
|
||
return buffer | ||
end | ||
|
||
return ziputils |