From c5cbd06020dadbd5da338af4d7545dabe89bbe37 Mon Sep 17 00:00:00 2001 From: Robert Patterson Date: Fri, 18 Oct 2024 11:06:55 -0500 Subject: [PATCH 1/5] problems with parts.xml remain --- docs/rgp-lua/finenv-properties.md | 12 ++ src/document_options_to_musescore.lua | 8 +- src/library/client.lua | 66 ++++++++- src/library/enigmaxml.lua | 114 +++++++++++++++ src/library/general_library.lua | 10 +- src/library/utils.lua | 1 + src/library/ziputils.lua | 197 ++++++-------------------- src/musicxml_massage_export.lua | 14 +- 8 files changed, 242 insertions(+), 180 deletions(-) create mode 100644 src/library/enigmaxml.lua diff --git a/docs/rgp-lua/finenv-properties.md b/docs/rgp-lua/finenv-properties.md index 49923a1f..c64591d3 100644 --- a/docs/rgp-lua/finenv-properties.md +++ b/docs/rgp-lua/finenv-properties.md @@ -82,6 +82,18 @@ end --- +#### EmbeddedLuaOSUtils\* (read-only property) + +If this property is true, it signifies that the `luaosutils` library is embedded in this version +of _RGP Lua_ and can be successfully accessed with a `require` statement. You are also guaranteed +that the minimum version of the embedded `luaosutils` is `2.2.0`. + +```lua +local luaosutils = finenv.EmbeddedLuaOSUtils and require('luaosutils') +``` + +--- + #### EndUndoBlock\* (function) Ends the currently active Undo/Redo block in Finale (if any). Finale will only store Undo/Redo blocks that contain edit changes to the documents. These calls cannot be nested. If your script will make further changes to the document after this call, it should call `StartNewUndoBlock()` again before making them. Otherwise, Finale's Undo stack could become corrupted. diff --git a/src/document_options_to_musescore.lua b/src/document_options_to_musescore.lua index 1e1cd9f4..e307f1d7 100644 --- a/src/document_options_to_musescore.lua +++ b/src/document_options_to_musescore.lua @@ -54,11 +54,10 @@ end -- luacheck: ignore 11./global_dialog -local text = require("luaosutils").text - local mixin = require("library.mixin") local enigma_string = require("library.enigma_string") local utils = require("library.utils") +local client = require("library.client") do_folder = do_folder or false @@ -68,7 +67,7 @@ local MUSX_EXTENSION = ".musx" local MUS_EXTENSION = ".mus" local TEXT_EXTENSION = ".mss" local PART_EXTENSION = ".part" .. TEXT_EXTENSION -local MSS_VERSION = "4.40" +local MSS_VERSION = "4.50" -- hard-coded scaling values local EVPU_PER_INCH = 288 @@ -380,6 +379,7 @@ function write_note_related_prefs(style_element) set_element_text(style_element, "graceNoteMag", size_prefs.GraceNoteSize / 100) set_element_text(style_element, "concertPitch", part_scope_prefs.DisplayInConcertPitch) set_element_text(style_element, "multiVoiceRestTwoSpaceOffset", math.abs(layer_one_prefs.RestOffset) >= 4) + set_element_text(style_element, "mergeMatchingRests", misc_prefs.ConsolidateRestsAcrossLayers) end function write_smart_shape_prefs(style_element) @@ -783,7 +783,7 @@ function document_options_to_musescore() end local selected_directory = select_directory() if selected_directory then - logfile_path = text.convert_encoding(selected_directory, text.get_utf8_codepage(), text.get_default_codepage()) .. LOGFILE_NAME + logfile_path = client.encode_with_client_codepage(selected_directory) .. LOGFILE_NAME local file = io.open(logfile_path, "w") if not file then error("unable to create logfile " .. logfile_path) diff --git a/src/library/client.lua b/src/library/client.lua index ccc4cf8d..0bc950db 100644 --- a/src/library/client.lua +++ b/src/library/client.lua @@ -164,20 +164,19 @@ end --[[ % encode_with_client_codepage -If the client supports LuaOSUtils, the filepath is encoded from UTF-8 to the current client -encoding. On macOS, this is always also UTF-8, so the situation where the string may be re-encoded -is only on Windows. (Recent versions of Windows also allow UTF-8 as the client encoding, so it may +If the client supports `luaosutils`, the filepath is encoded from utf8 to the current client +encoding. On macOS, this is always also utf8, so the situation where the string may be re-encoded +is only on Windows. (Recent versions of Windows also allow utf8 as the client encoding, so it may not be re-encoded even on Windows.) -If LuaOSUtils is not available, the string is returned unchanged. +If `luaosutils` is not available, the string is returned unchanged. A primary use-case for this function is filepaths. Windows requires 8-bit filepaths to be encoded with the client codepage. -@ input_string (string) the UTF-encoded string to re-encode -: (string) the string re-encoded with the clieng codepage +@ input_string (string) the utf8-encoded string to re-encode +: (string) the string re-encoded with the client codepage ]] - function client.encode_with_client_codepage(input_string) if client.supports("luaosutils") then local text = require("luaosutils").text @@ -188,4 +187,57 @@ function client.encode_with_client_codepage(input_string) return input_string end +--[[ +% encode_with_utf8_codepage + +If the client supports `luaosutils`, the filepath is encoded from the current client encoding +to utf8. On macOS, the client encoding is always also utf8, so the situation where the string may +be re-encoded is only on Windows. (Recent versions of Windows also allow utf8 as the client encoding, so it may +not be re-encoded even on Windows.) + +If `luaosutils` is not available, the string is returned unchanged. + +A primary use-case for this function is filepaths. Windows requires 8-bit filepaths to be encoded +with the client codepage. + +@ input_string (string) the client-encoded string to re-encode +: (string) the string re-encoded with the utf8 codepage +]] +function client.encode_with_utf8_codepage(input_string) + if client.supports("luaosutils") then + local text = require("luaosutils").text + if text and text.get_default_codepage() ~= text.get_utf8_codepage() then + return text.convert_encoding(input_string, text.get_default_codepage(), text.get_utf8_codepage()) + end + end + return input_string +end + +--[[ +% execute + +If the client supports `luaosutils`, the command is executed using `luaosutils.execute`. Otherwise it uses `io.popen`. +In either case, the output from the command is returned. + +Starting with v0.67, this function throws an error if the script is not trusted or has not set +`finaleplugin.ExecuteExternalCode` to `true`. + +@ command (string) The command to execute encoded with **client encoding**. +: (string) The `stdout` from the command, in whatever encoding it generated. +]] +function client.execute(command) + if client.supports("luaosutils") then + local process = require("luaosutils").process + if process then + return process.execute(command) + end + end + print("popen " .. command) + local handle = io.popen(command) + if not handle then return nil end + local retval = handle:read("*a") + handle:close() + return retval +end + return client diff --git a/src/library/enigmaxml.lua b/src/library/enigmaxml.lua new file mode 100644 index 00000000..79db4ad2 --- /dev/null +++ b/src/library/enigmaxml.lua @@ -0,0 +1,114 @@ +--[[ +$module 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/Project-Attacca/enigmaxml-documentation) +repository. +]] -- +local enigmaxml = {} + +local utils = require("library.utils") +local client = require("library.client") +local ziputils = require("library.ziputils") + +-- symmetrical encryption/decryption function for EnigmaXML +local function crypt_enigmaxml_buffer(buffer) + -- do not use because this library must be loadable by Lua 5.2 (JW Lua) + local INITIAL_STATE = 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 enigmaxml.extract_enigmaxml(filepath) + local not_supported_message + if not client.supports("luaosutils") and false then --finenv.UI():IsOnWindows() then + -- io.popen doesn't work with our Windows PowerShell commands + not_supported_message = "enigmaxma.extract_enigmaxml requires embedded luaosutils" + elseif finenv.TrustedMode == finenv.TrustedModeType.UNTRUSTED then + not_supported_message = "enigmaxml.extract_enigmaxml must run in Trusted mode." + elseif not finaleplugin.ExecuteExternalCode then + not_supported_message = "enigmaxml.extract_enigmaxml must have finaleplugin.ExecuteExternalCode set to true." + end + 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 + + -- Steps to extract: + -- Unzip the `.musx` (which is `.zip` in disguise) + -- Run the `score.dat` file through `crypt_enigmaxml_buffer` to get a gzip archive of the EnigmaXML file. + -- Gunzip the extracted EnigmaXML gzip archive into a string and return it. + + local os_filepath = client.encode_with_client_codepage(filepath) + local output_dir, zipcommand = ziputils.calc_temp_output_path(os_filepath) + if not client.execute(zipcommand) then + error(zipcommand .. " failed") + end + + -- do not use because this library must be loadable by Lua 5.2 (JW Lua) + local file = 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) + client.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 = 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 = client.execute(gunzip_command) + client.execute(ziputils.calc_delete_file_command(gzip_path)) + if not buffer or buffer == "" then + error(gunzip_command .. " failed") + end + end + + return buffer +end + +return enigmaxml diff --git a/src/library/general_library.lua b/src/library/general_library.lua index 9c556e7b..6dc956ad 100644 --- a/src/library/general_library.lua +++ b/src/library/general_library.lua @@ -330,7 +330,7 @@ the .json files for each font. The table is in the format: ]] function library.get_smufl_font_list() - local osutils = finenv.EmbeddedLuaOSUtils and require("luaosutils") + local osutils = client.supports("luaosutils") and require("luaosutils") local font_names = {} local add_to_table = function(for_user) local smufl_directory = calc_smufl_directory(for_user) @@ -339,13 +339,9 @@ function library.get_smufl_font_list() if osutils then return osutils.process.list_dir(smufl_directory, options) end - -- Starting in 0.67, io.popen may fail due to being untrusted. + -- Starting in 0.67, execute may fail due to being untrusted. local cmd = finenv.UI():IsOnWindows() and "dir " or "ls " - local handle = io.popen(cmd .. options .. " \"" .. smufl_directory .. "\"") - if not handle then return "" end - local retval = handle:read("*a") - handle:close() - return retval + return client.execute(cmd .. options .. " \"" .. smufl_directory .. "\"") or "" end local is_font_available = function(dir) local fc_dir = finale.FCString() diff --git a/src/library/utils.lua b/src/library/utils.lua index cfc90225..7f988127 100644 --- a/src/library/utils.lua +++ b/src/library/utils.lua @@ -533,6 +533,7 @@ function utils.eachfile(directory_path, recursive) fcstr:AssureEndingPathDelimiter() directory_path = fcstr.LuaString + -- direcly call text.convert_encoding to avoid dependency on library.utils local lfs_directory_path = text.convert_encoding(directory_path, text.get_utf8_codepage(), text.get_default_codepage()) return coroutine.wrap(function() diff --git a/src/library/ziputils.lua b/src/library/ziputils.lua index 47007bc9..d3a359fc 100644 --- a/src/library/ziputils.lua +++ b/src/library/ziputils.lua @@ -5,49 +5,36 @@ Functions for unzipping files. (Future may include zipping as well.) Dependencies: -- The Windows version uses `PowerShell`. +- The Windows version uses `PowerShell 5`. Users of Windows 8.1 must manually upgrade +to this version of PowerShell. - The macOS version uses `unzip` and `gunzip`. -- In both cases the necessary tools are pre-installed with a typical installation of any version +- Except as noted, the necessary tools are pre-installed with a typical installation of any version of the OS that supports 64-bit Finale. - -Pay careful attention to the comments about how strings are encoded. They are either encoded -**platform** or **utf8**. On macOS, platform encoding is always utf8, 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. (Use the `get_default_codepage` function to get -the platform encoding.) The `luaosutils.process.execute` function requires platform encoding as do -`lfs` and all built-in Lua `os` and `io` functions that take strings as input. - -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. +- The PowerShell commands are not compatible with `io.popen`, so Windows callers should check `client.supports("luaosutils")` +before calling `client.execute` with these commands. + +Thie library expects strings to be client-encoded. On macOS, client encoding is always utf8, +but on Windows it can be any number of encodings depending on the locale settings and version of Windows. +You can use `client.encode_with_client_codepage` to convert a utf8 string to client encoding. +The `client.execute` function requires client encoding as do `lfs` and all built-in +Lua `os` and `io` functions that take strings as input. ]] -- 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 +local client = require("library.client") --[[ % calc_rmdir_command Returns the platform-dependent command to remove a directory. It can be passed -to `luaosutils.process.execute`. +to `client.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. +@ path_to_remove (string) client-encoded path of directory to remove. +: (string) client-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 @@ -57,61 +44,54 @@ end % calc_delete_file_command Returns the platform-dependent command to delete a file. It can be passed -to `luaosutils.process.execute`. +to `client.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. +@ path_to_remove (string) client-encoded path of directory to remove. +: (string) client-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. +passed to `client.execute` to unzip the input archive into the temporary name as a directory. 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. +@ [archive_path] (string) client-encoded filepath to the zip archive that is included in the zip command. +: (string) client-encoded temporary path generated by the system. +: (string) client-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) + client.execute(rmcommand) local zipcommand if finenv.UI():IsOnMac() then zipcommand = "unzip \"" .. archive_path .. "\" -d " .. output_dir else - zipcommand = [[ - $archivePath = '%s' - $outputDir = '%s' - $zipPath = $archivePath + '.zip' - Copy-Item -Path $archivePath -Destination $zipPath - Expand-Archive -Path $zipPath -DestinationPath $outputDir - Remove-Item -Path $zipPath - ]] + zipcommand = table.concat({ + "$archivePath = '%s'", + "$outputDir = '%s'", + "$zipPath = $archivePath + '.zip'", + "Copy-Item -Path $archivePath -Destination $zipPath", + "Expand-Archive -Path $zipPath -DestinationPath $outputDir", + "Remove-Item -Path $zipPath", + }, "; ") zipcommand = string.format(zipcommand, archive_path, output_dir) - zipcommand = string.format("powershell -c & { %s }", zipcommand) + zipcommand = string.format("powershell -c \"%s\"", zipcommand) end return output_dir, zipcommand end @@ -120,24 +100,24 @@ end % calc_gunzip_command Returns the platform-dependent command to gunzip a file to `stdout`. It can be passed -to `luaosutils.process.execute`, which will then return the text directly. +to `client.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. +@ archive_path (string) client-encoded path of source gzip archive. +: (string) client-encoded command string to execute. ]] function ziputils.calc_gunzip_command(archive_path) if finenv.UI():IsOnMac() then return "gunzip -c " .. archive_path else - local command = [[ - $fs = New-Object IO.Filestream('%s',([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read)) - $gz = New-Object IO.Compression.GzipStream($fs,[IO.Compression.CompressionMode]::Decompress) - $sr = New-Object IO.StreamReader($gz) - while (-not $sr.EndOfStream) { Write-Output $sr.ReadLine() } - $sr.Close() - ]] + local command = table.concat({ + "$fs = New-Object IO.Filestream('%s',([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read))", + "$gz = New-Object IO.Compression.GzipStream($fs,[IO.Compression.CompressionMode]::Decompress)", + "$sr = New-Object IO.StreamReader($gz)", + "while (-not $sr.EndOfStream) { Write-Output $sr.ReadLine() }", + "$sr.Close()" + }, "; ") command = string.format(command, archive_path) - return string.format("powershell -c & { %s }", command) + return string.format("powershell -c \"%s\"", command) end end @@ -154,95 +134,4 @@ function ziputils.calc_is_gzip(buffer) 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 = 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 - - -- Steps to extract: - -- Unzip the `.musx` (which is `.zip` in disguise) - -- Run the `score.dat` file through `crypt_enigmaxml_buffer` to get a gzip archive of the EnigmaXML file. - -- Gunzip the extracted EnigmaXML gzip archive into a string and return it. - - 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 = 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 = 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 diff --git a/src/musicxml_massage_export.lua b/src/musicxml_massage_export.lua index 3beb8e66..7cbca15c 100644 --- a/src/musicxml_massage_export.lua +++ b/src/musicxml_massage_export.lua @@ -58,10 +58,10 @@ function plugindef() end local lfs = require("lfs") -local text = require("luaosutils").text local utils = require("library.utils") local mixin = require("library.mixin") +local client = require("library.client") do_single_file = do_single_file or false local XML_EXTENSION = ".musicxml" @@ -111,7 +111,7 @@ function log_message(msg, is_error) end local function remove_processing_instructions(input_name, output_name) - local input_file = io.open(text.convert_encoding(input_name, text.get_utf8_codepage(), text.get_default_codepage()), "r") + local input_file = io.open(client.encode_with_client_codepage(input_name), "r") if not input_file then error("Cannot open file: " .. input_name) end @@ -125,8 +125,7 @@ local function remove_processing_instructions(input_name, output_name) end end input_file:close() - local output_file = io.open( - text.convert_encoding(output_name, text.get_utf8_codepage(), text.get_default_codepage()), "w") + local output_file = io.open(client.encode_with_client_codepage(output_name), "w") if not output_file then error("Cannot open file for writing: " .. output_name) end @@ -437,8 +436,7 @@ function process_one_file(input_file) local output_file = path .. filename .. ADD_TO_FILENAME .. XML_EXTENSION local document_path = (function() local function exist(try_path) - local attr = lfs.attributes(text.convert_encoding(try_path, text.get_utf8_codepage(), - text.get_default_codepage())) + local attr = lfs.attributes(client.encode_with_client_codepage(try_path)) return attr and attr.mode == "file" end local try_path = path .. filename .. ".musx" @@ -467,7 +465,7 @@ function process_one_file(input_file) local function abort_if(condition, msg) if condition then log_message(msg, true) - os.remove(text.convert_encoding(output_file, text.get_utf8_codepage(), text.get_default_codepage())) -- delete erroneous file + os.remove(client.encode_with_client_codepage(output_file)) -- delete erroneous file close_document() return true end @@ -543,7 +541,7 @@ function process_files(file_list, selected_path) dialog:CreateCancelButton("cancel") -- registrations dialog:RegisterInitWindow(function(self) - logfile_path = text.convert_encoding(selected_path, text.get_utf8_codepage(), text.get_default_codepage()) .. LOGFILE_NAME + logfile_path = client.encode_with_client_codepage(selected_path) .. LOGFILE_NAME local file = io.open(logfile_path, "w") if not file then error("unable to create logfile " .. logfile_path) From 4652c4094f404737bed5f06779447cd6a88c0b7d Mon Sep 17 00:00:00 2001 From: Robert Patterson Date: Fri, 18 Oct 2024 11:48:28 -0500 Subject: [PATCH 2/5] wip --- src/library/enigmaxml.lua | 5 +---- src/library/ziputils.lua | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/library/enigmaxml.lua b/src/library/enigmaxml.lua index 79db4ad2..c9311546 100644 --- a/src/library/enigmaxml.lua +++ b/src/library/enigmaxml.lua @@ -53,10 +53,7 @@ older `.mus` format. ]] function enigmaxml.extract_enigmaxml(filepath) local not_supported_message - if not client.supports("luaosutils") and false then --finenv.UI():IsOnWindows() then - -- io.popen doesn't work with our Windows PowerShell commands - not_supported_message = "enigmaxma.extract_enigmaxml requires embedded luaosutils" - elseif finenv.TrustedMode == finenv.TrustedModeType.UNTRUSTED then + if finenv.TrustedMode == finenv.TrustedModeType.UNTRUSTED then not_supported_message = "enigmaxml.extract_enigmaxml must run in Trusted mode." elseif not finaleplugin.ExecuteExternalCode then not_supported_message = "enigmaxml.extract_enigmaxml must have finaleplugin.ExecuteExternalCode set to true." diff --git a/src/library/ziputils.lua b/src/library/ziputils.lua index d3a359fc..5e501193 100644 --- a/src/library/ziputils.lua +++ b/src/library/ziputils.lua @@ -10,8 +10,6 @@ to this version of PowerShell. - The macOS version uses `unzip` and `gunzip`. - Except as noted, the necessary tools are pre-installed with a typical installation of any version of the OS that supports 64-bit Finale. -- The PowerShell commands are not compatible with `io.popen`, so Windows callers should check `client.supports("luaosutils")` -before calling `client.execute` with these commands. Thie library expects strings to be client-encoded. On macOS, client encoding is always utf8, but on Windows it can be any number of encodings depending on the locale settings and version of Windows. From 4d17455fd971aefb44adacd0519e533d74a67e2d Mon Sep 17 00:00:00 2001 From: Robert Patterson Date: Fri, 18 Oct 2024 14:56:11 -0500 Subject: [PATCH 3/5] PowerShell script now working with io.popen --- src/library/client.lua | 1 - src/library/ziputils.lua | 13 ++++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/library/client.lua b/src/library/client.lua index 0bc950db..918382a5 100644 --- a/src/library/client.lua +++ b/src/library/client.lua @@ -232,7 +232,6 @@ function client.execute(command) return process.execute(command) end end - print("popen " .. command) local handle = io.popen(command) if not handle then return nil end local retval = handle:read("*a") diff --git a/src/library/ziputils.lua b/src/library/ziputils.lua index 5e501193..5e86376c 100644 --- a/src/library/ziputils.lua +++ b/src/library/ziputils.lua @@ -108,11 +108,14 @@ function ziputils.calc_gunzip_command(archive_path) return "gunzip -c " .. archive_path else local command = table.concat({ - "$fs = New-Object IO.Filestream('%s',([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read))", - "$gz = New-Object IO.Compression.GzipStream($fs,[IO.Compression.CompressionMode]::Decompress)", - "$sr = New-Object IO.StreamReader($gz)", - "while (-not $sr.EndOfStream) { Write-Output $sr.ReadLine() }", - "$sr.Close()" + "$fs = New-Object IO.FileStream('%s', [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)", + "$gz = New-Object IO.Compression.GzipStream($fs, [IO.Compression.CompressionMode]::Decompress)", + "$buffer = New-Object byte[] 4096", -- Define a buffer size (e.g., 4096 bytes) + "while (($read = $gz.Read($buffer, 0, $buffer.Length)) -gt 0) {", + " [Console]::OpenStandardOutput().Write($buffer, 0, $read)", + "}", + "$gz.Close()", + "$fs.Close()" }, "; ") command = string.format(command, archive_path) return string.format("powershell -c \"%s\"", command) From e84122bd75ff0a180d812d4ecb45e84a88f8b485 Mon Sep 17 00:00:00 2001 From: Robert Patterson Date: Fri, 18 Oct 2024 15:02:14 -0500 Subject: [PATCH 4/5] fix comments --- src/library/enigmaxml.lua | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/library/enigmaxml.lua b/src/library/enigmaxml.lua index c9311546..29bc7e4c 100644 --- a/src/library/enigmaxml.lua +++ b/src/library/enigmaxml.lua @@ -40,16 +40,11 @@ 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`. +: (string) buffer of EnigmaXml data extracted from the `.musx`. (The xml declaration specifies the encoding, but expect it to be utf8.) ]] function enigmaxml.extract_enigmaxml(filepath) local not_supported_message @@ -67,7 +62,7 @@ function enigmaxml.extract_enigmaxml(filepath) end -- Steps to extract: - -- Unzip the `.musx` (which is `.zip` in disguise) + -- Unzip the `.musx` (which is a `.zip` archive in disguise) -- Run the `score.dat` file through `crypt_enigmaxml_buffer` to get a gzip archive of the EnigmaXML file. -- Gunzip the extracted EnigmaXML gzip archive into a string and return it. From 1aa45d02b42f72e27182e632f06af0070fd16c82 Mon Sep 17 00:00:00 2001 From: Robert Patterson Date: Sun, 20 Oct 2024 18:06:18 -0500 Subject: [PATCH 5/5] update date and version on changed script --- src/document_options_to_musescore.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/document_options_to_musescore.lua b/src/document_options_to_musescore.lua index e307f1d7..5082c1ca 100644 --- a/src/document_options_to_musescore.lua +++ b/src/document_options_to_musescore.lua @@ -4,8 +4,8 @@ function plugindef() finaleplugin.NoStore = true finaleplugin.Author = "Robert Patterson" finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/" - finaleplugin.Version = "1.0.1" - finaleplugin.Date = "October 6, 2024" + finaleplugin.Version = "1.0.2" + finaleplugin.Date = "October 20, 2024" finaleplugin.CategoryTags = "Document" finaleplugin.MinJWLuaVersion = 0.75 finaleplugin.Notes = [[