Skip to content

Commit 0dafdaf

Browse files
authored
Merge pull request #775 from rpatters1/rpatters1/refactor-dependencies-with-client-lib
Refactor ziputils
2 parents c8e6f76 + 1aa45d0 commit 0dafdaf

8 files changed

+235
-181
lines changed

docs/rgp-lua/finenv-properties.md

+12
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ end
8282

8383
---
8484

85+
#### EmbeddedLuaOSUtils\* (read-only property)
86+
87+
If this property is true, it signifies that the `luaosutils` library is embedded in this version
88+
of _RGP Lua_ and can be successfully accessed with a `require` statement. You are also guaranteed
89+
that the minimum version of the embedded `luaosutils` is `2.2.0`.
90+
91+
```lua
92+
local luaosutils = finenv.EmbeddedLuaOSUtils and require('luaosutils')
93+
```
94+
95+
---
96+
8597
#### EndUndoBlock\* (function)
8698

8799
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.

src/document_options_to_musescore.lua

+6-6
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ function plugindef()
44
finaleplugin.NoStore = true
55
finaleplugin.Author = "Robert Patterson"
66
finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
7-
finaleplugin.Version = "1.0.1"
8-
finaleplugin.Date = "October 6, 2024"
7+
finaleplugin.Version = "1.0.2"
8+
finaleplugin.Date = "October 20, 2024"
99
finaleplugin.CategoryTags = "Document"
1010
finaleplugin.MinJWLuaVersion = 0.75
1111
finaleplugin.Notes = [[
@@ -54,11 +54,10 @@ end
5454

5555
-- luacheck: ignore 11./global_dialog
5656

57-
local text = require("luaosutils").text
58-
5957
local mixin = require("library.mixin")
6058
local enigma_string = require("library.enigma_string")
6159
local utils = require("library.utils")
60+
local client = require("library.client")
6261

6362
do_folder = do_folder or false
6463

@@ -68,7 +67,7 @@ local MUSX_EXTENSION <const> = ".musx"
6867
local MUS_EXTENSION <const> = ".mus"
6968
local TEXT_EXTENSION <const> = ".mss"
7069
local PART_EXTENSION <const> = ".part" .. TEXT_EXTENSION
71-
local MSS_VERSION <const> = "4.40"
70+
local MSS_VERSION <const> = "4.50"
7271

7372
-- hard-coded scaling values
7473
local EVPU_PER_INCH <const> = 288
@@ -380,6 +379,7 @@ function write_note_related_prefs(style_element)
380379
set_element_text(style_element, "graceNoteMag", size_prefs.GraceNoteSize / 100)
381380
set_element_text(style_element, "concertPitch", part_scope_prefs.DisplayInConcertPitch)
382381
set_element_text(style_element, "multiVoiceRestTwoSpaceOffset", math.abs(layer_one_prefs.RestOffset) >= 4)
382+
set_element_text(style_element, "mergeMatchingRests", misc_prefs.ConsolidateRestsAcrossLayers)
383383
end
384384

385385
function write_smart_shape_prefs(style_element)
@@ -783,7 +783,7 @@ function document_options_to_musescore()
783783
end
784784
local selected_directory = select_directory()
785785
if selected_directory then
786-
logfile_path = text.convert_encoding(selected_directory, text.get_utf8_codepage(), text.get_default_codepage()) .. LOGFILE_NAME
786+
logfile_path = client.encode_with_client_codepage(selected_directory) .. LOGFILE_NAME
787787
local file <close> = io.open(logfile_path, "w")
788788
if not file then
789789
error("unable to create logfile " .. logfile_path)

src/library/client.lua

+58-7
Original file line numberDiff line numberDiff line change
@@ -164,20 +164,19 @@ end
164164
--[[
165165
% encode_with_client_codepage
166166
167-
If the client supports LuaOSUtils, the filepath is encoded from UTF-8 to the current client
168-
encoding. On macOS, this is always also UTF-8, so the situation where the string may be re-encoded
169-
is only on Windows. (Recent versions of Windows also allow UTF-8 as the client encoding, so it may
167+
If the client supports `luaosutils`, the filepath is encoded from utf8 to the current client
168+
encoding. On macOS, this is always also utf8, so the situation where the string may be re-encoded
169+
is only on Windows. (Recent versions of Windows also allow utf8 as the client encoding, so it may
170170
not be re-encoded even on Windows.)
171171
172-
If LuaOSUtils is not available, the string is returned unchanged.
172+
If `luaosutils` is not available, the string is returned unchanged.
173173
174174
A primary use-case for this function is filepaths. Windows requires 8-bit filepaths to be encoded
175175
with the client codepage.
176176
177-
@ input_string (string) the UTF-encoded string to re-encode
178-
: (string) the string re-encoded with the clieng codepage
177+
@ input_string (string) the utf8-encoded string to re-encode
178+
: (string) the string re-encoded with the client codepage
179179
]]
180-
181180
function client.encode_with_client_codepage(input_string)
182181
if client.supports("luaosutils") then
183182
local text = require("luaosutils").text
@@ -188,4 +187,56 @@ function client.encode_with_client_codepage(input_string)
188187
return input_string
189188
end
190189

190+
--[[
191+
% encode_with_utf8_codepage
192+
193+
If the client supports `luaosutils`, the filepath is encoded from the current client encoding
194+
to utf8. On macOS, the client encoding is always also utf8, so the situation where the string may
195+
be re-encoded is only on Windows. (Recent versions of Windows also allow utf8 as the client encoding, so it may
196+
not be re-encoded even on Windows.)
197+
198+
If `luaosutils` is not available, the string is returned unchanged.
199+
200+
A primary use-case for this function is filepaths. Windows requires 8-bit filepaths to be encoded
201+
with the client codepage.
202+
203+
@ input_string (string) the client-encoded string to re-encode
204+
: (string) the string re-encoded with the utf8 codepage
205+
]]
206+
function client.encode_with_utf8_codepage(input_string)
207+
if client.supports("luaosutils") then
208+
local text = require("luaosutils").text
209+
if text and text.get_default_codepage() ~= text.get_utf8_codepage() then
210+
return text.convert_encoding(input_string, text.get_default_codepage(), text.get_utf8_codepage())
211+
end
212+
end
213+
return input_string
214+
end
215+
216+
--[[
217+
% execute
218+
219+
If the client supports `luaosutils`, the command is executed using `luaosutils.execute`. Otherwise it uses `io.popen`.
220+
In either case, the output from the command is returned.
221+
222+
Starting with v0.67, this function throws an error if the script is not trusted or has not set
223+
`finaleplugin.ExecuteExternalCode` to `true`.
224+
225+
@ command (string) The command to execute encoded with **client encoding**.
226+
: (string) The `stdout` from the command, in whatever encoding it generated.
227+
]]
228+
function client.execute(command)
229+
if client.supports("luaosutils") then
230+
local process = require("luaosutils").process
231+
if process then
232+
return process.execute(command)
233+
end
234+
end
235+
local handle = io.popen(command)
236+
if not handle then return nil end
237+
local retval = handle:read("*a")
238+
handle:close()
239+
return retval
240+
end
241+
191242
return client

src/library/enigmaxml.lua

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
--[[
2+
$module enigmaxml
3+
4+
EnigmaXML 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/Project-Attacca/enigmaxml-documentation)
7+
repository.
8+
]] --
9+
local enigmaxml = {}
10+
11+
local utils = require("library.utils")
12+
local client = require("library.client")
13+
local ziputils = require("library.ziputils")
14+
15+
-- symmetrical encryption/decryption function for EnigmaXML
16+
local function crypt_enigmaxml_buffer(buffer)
17+
-- do not use <const> because this library must be loadable by Lua 5.2 (JW Lua)
18+
local INITIAL_STATE = 0x28006D45 -- this value was determined empirically
19+
local state = INITIAL_STATE
20+
local result = {}
21+
22+
for i = 1, #buffer do
23+
-- BSD rand()
24+
if (i - 1) % 0x20000 == 0 then
25+
state = INITIAL_STATE
26+
end
27+
state = (state * 0x41c64e6d + 0x3039) & 0xFFFFFFFF -- Simulate 32-bit overflow
28+
local upper = state >> 16
29+
local c = upper + math.floor(upper / 255)
30+
31+
local byte = string.byte(buffer, i)
32+
byte = byte ~ (c & 0xFF) -- XOR operation on the byte
33+
34+
table.insert(result, string.char(byte))
35+
end
36+
37+
return table.concat(result)
38+
end
39+
40+
--[[
41+
% extract_enigmaxml
42+
43+
This function extracts the EnigmaXML buffer from a `.musx` file. Note that it does not work with Finale's
44+
older `.mus` format.
45+
46+
@ filepath (string) utf8-encoded file path to a `.musx` file.
47+
: (string) buffer of EnigmaXml data extracted from the `.musx`. (The xml declaration specifies the encoding, but expect it to be utf8.)
48+
]]
49+
function enigmaxml.extract_enigmaxml(filepath)
50+
local not_supported_message
51+
if finenv.TrustedMode == finenv.TrustedModeType.UNTRUSTED then
52+
not_supported_message = "enigmaxml.extract_enigmaxml must run in Trusted mode."
53+
elseif not finaleplugin.ExecuteExternalCode then
54+
not_supported_message = "enigmaxml.extract_enigmaxml must have finaleplugin.ExecuteExternalCode set to true."
55+
end
56+
if not_supported_message then
57+
error(not_supported_message, 2)
58+
end
59+
local _, _, extension = utils.split_file_path(filepath)
60+
if extension ~= ".musx" then
61+
error(filepath .. " is not a .musx file.", 2)
62+
end
63+
64+
-- Steps to extract:
65+
-- Unzip the `.musx` (which is a `.zip` archive in disguise)
66+
-- Run the `score.dat` file through `crypt_enigmaxml_buffer` to get a gzip archive of the EnigmaXML file.
67+
-- Gunzip the extracted EnigmaXML gzip archive into a string and return it.
68+
69+
local os_filepath = client.encode_with_client_codepage(filepath)
70+
local output_dir, zipcommand = ziputils.calc_temp_output_path(os_filepath)
71+
if not client.execute(zipcommand) then
72+
error(zipcommand .. " failed")
73+
end
74+
75+
-- do not use <close> because this library must be loadable by Lua 5.2 (JW Lua)
76+
local file = io.open(output_dir .. "/score.dat", "rb")
77+
if not file then
78+
error("unable to read " .. output_dir .. "/score.dat")
79+
end
80+
local buffer = file:read("*all")
81+
file:close()
82+
83+
local delcommand = ziputils.calc_rmdir_command(output_dir)
84+
client.execute(delcommand)
85+
86+
buffer = crypt_enigmaxml_buffer(buffer)
87+
if ziputils.calc_is_gzip(buffer) then
88+
local gzip_path = ziputils.calc_temp_output_path()
89+
local gzip_file = io.open(gzip_path, "wb")
90+
if not gzip_file then
91+
error("unable to create " .. gzip_file)
92+
end
93+
gzip_file:write(buffer)
94+
gzip_file:close()
95+
local gunzip_command = ziputils.calc_gunzip_command(gzip_path)
96+
buffer = client.execute(gunzip_command)
97+
client.execute(ziputils.calc_delete_file_command(gzip_path))
98+
if not buffer or buffer == "" then
99+
error(gunzip_command .. " failed")
100+
end
101+
end
102+
103+
return buffer
104+
end
105+
106+
return enigmaxml

src/library/general_library.lua

+3-7
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ the .json files for each font. The table is in the format:
330330
]]
331331

332332
function library.get_smufl_font_list()
333-
local osutils = finenv.EmbeddedLuaOSUtils and require("luaosutils")
333+
local osutils = client.supports("luaosutils") and require("luaosutils")
334334
local font_names = {}
335335
local add_to_table = function(for_user)
336336
local smufl_directory = calc_smufl_directory(for_user)
@@ -339,13 +339,9 @@ function library.get_smufl_font_list()
339339
if osutils then
340340
return osutils.process.list_dir(smufl_directory, options)
341341
end
342-
-- Starting in 0.67, io.popen may fail due to being untrusted.
342+
-- Starting in 0.67, execute may fail due to being untrusted.
343343
local cmd = finenv.UI():IsOnWindows() and "dir " or "ls "
344-
local handle = io.popen(cmd .. options .. " \"" .. smufl_directory .. "\"")
345-
if not handle then return "" end
346-
local retval = handle:read("*a")
347-
handle:close()
348-
return retval
344+
return client.execute(cmd .. options .. " \"" .. smufl_directory .. "\"") or ""
349345
end
350346
local is_font_available = function(dir)
351347
local fc_dir = finale.FCString()

src/library/utils.lua

+1
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ function utils.eachfile(directory_path, recursive)
533533
fcstr:AssureEndingPathDelimiter()
534534
directory_path = fcstr.LuaString
535535

536+
-- direcly call text.convert_encoding to avoid dependency on library.utils
536537
local lfs_directory_path = text.convert_encoding(directory_path, text.get_utf8_codepage(), text.get_default_codepage())
537538

538539
return coroutine.wrap(function()

0 commit comments

Comments
 (0)