diff --git a/template/src/lib/utils.lua b/template/src/lib/utils.lua index 1172364..5c545e5 100644 --- a/template/src/lib/utils.lua +++ b/template/src/lib/utils.lua @@ -163,6 +163,9 @@ end --- @param allowEmptyValues? boolean Allows empty strings as parameter values (optional). Defaults to false. --- @param logCommand? boolean If false, prevents logging of the command's content (optional). Defaults to true. function SendToDevice(deviceId, strCommand, tParams, allowEmptyValues, logCommand) + if logCommand ~= nil then + allowEmptyValues = allowEmptyValues == true + end return C4Call("SendToDevice", deviceId, strCommand, tParams, allowEmptyValues, logCommand) end @@ -173,6 +176,9 @@ end --- @param strMessage? string Overrides message type ("COMMAND" or "NOTIFY") (optional). Defaults to "COMMAND". --- @param allowEmptyValues? boolean Allows empty values in message parameters (optional). function SendToProxy(idBinding, strCommand, tParams, strMessage, allowEmptyValues) + if allowEmptyValues ~= nil then + strMessage = strMessage or "COMMAND" + end return C4Call("SendToProxy", idBinding, strCommand, tParams, strMessage, allowEmptyValues) end @@ -184,7 +190,7 @@ end --- @param allowEmptyValues? boolean Allows empty strings as parameter values (optional). Defaults to false. --- @param logCommand? boolean If true, lets C4 log the command's content (optional). Defaults to false. function SendSecretToDevice(deviceId, strCommand, tParams, allowEmptyValues, logCommand) - return C4CallHiding("SendToDevice", 3, deviceId, strCommand, tParams, allowEmptyValues, logCommand == true) + return C4CallHiding("SendToDevice", 3, deviceId, strCommand, tParams, allowEmptyValues == true, logCommand == true) end --- SendToProxy for a message whose parameters hold a secret, such as a user code: its trace names the binding @@ -195,6 +201,9 @@ end --- @param strMessage? string Overrides message type ("COMMAND" or "NOTIFY") (optional). Defaults to "COMMAND". --- @param allowEmptyValues? boolean Allows empty values in message parameters (optional). function SendSecretToProxy(idBinding, strCommand, tParams, strMessage, allowEmptyValues) + if allowEmptyValues ~= nil then + strMessage = strMessage or "COMMAND" + end return C4CallHiding("SendToProxy", 3, idBinding, strCommand, tParams, strMessage, allowEmptyValues) end diff --git a/template/test/c4_shim.lua b/template/test/c4_shim.lua index 4e1c010..289b737 100644 --- a/template/test/c4_shim.lua +++ b/template/test/c4_shim.lua @@ -187,8 +187,32 @@ end function C4:GetDriverFileName() return "example.c4z" end -function C4:SendToDevice() end -function C4:SendToProxy() end +-- The controller checks the type of each argument it is handed, by place, so a nil handed as the 4th or 5th +-- argument is refused even when nothing follows it. The messages are the controller's (4.3.0, 2026-11-10). +function C4:SendToDevice(...) + local n, _, _, tParams, allowEmptyValues, logCommand = select("#", ...), ... + if n < 3 then + error("Error: Not Enough Parameters Passed (expected 3)", 0) + elseif type(tParams) ~= "table" then + error("tParams should be a table", 0) + elseif n >= 4 and type(allowEmptyValues) ~= "boolean" then + error("Error: 4th parameter must be a boolean value", 0) + elseif n >= 5 and type(logCommand) ~= "boolean" then + error("Error: 5th parameter must be a boolean value", 0) + end +end +function C4:SendToProxy(...) + local n, _, _, tParams, strMessage, allowEmptyValues = select("#", ...), ... + if n < 3 then + error("Error: Not Enough Parameters Passed (expected 3)", 0) + elseif type(tParams) ~= "table" and type(tParams) ~= "string" then + error("Error: 3rd parameter must be a string (strValue) or table (tParams)", 0) + elseif n >= 4 and type(strMessage) ~= "string" then + error("strCallType should be a string", 0) + elseif n >= 5 and type(allowEmptyValues) ~= "boolean" then + error("Error: 5th parameter must be a boolean value", 0) + end +end --------------------------------------------------------------------------- -- Network bindings diff --git a/template/test/test_c4_shim.lua b/template/test/test_c4_shim.lua index 2571249..e56417d 100644 --- a/template/test/test_c4_shim.lua +++ b/template/test/test_c4_shim.lua @@ -1373,6 +1373,72 @@ ShimSetDeviceHidden(20, true) T.eq("a hidden device is not listed", listed({ DeviceIds = "20" }), {}) ShimResetDevices() +-------------------------------------------------------------------------------- +T.section("C4:SendToDevice and C4:SendToProxy") +-------------------------------------------------------------------------------- +do + --- The message a call is refused with, or nil if it is accepted. + local function refusal(fn) + local ok, err = pcall(fn) + return not ok and err or nil + end + + local DEVICE_FOURTH = "Error: 4th parameter must be a boolean value" + local FIFTH = "Error: 5th parameter must be a boolean value" + local CALL_TYPE = "strCallType should be a string" + local TOO_FEW = "Error: Not Enough Parameters Passed (expected 3)" + + -- Each case: what it is, how many arguments follow the command, those arguments, the refusal or nil. + local deviceCalls = { + { "a table of parameters", 1, { {} }, nil }, + { "both flags", 3, { {}, false, false }, nil }, + { "both flags set", 3, { {}, true, true }, nil }, + { "a nil 4th before a 5th", 3, { {}, nil, false }, DEVICE_FOURTH }, + { "a nil 4th passed on", 2, { {}, nil }, DEVICE_FOURTH }, + { "a string 4th", 3, { {}, "x", false }, DEVICE_FOURTH }, + { "a nil 5th passed on", 3, { {}, false, nil }, FIFTH }, + { "two nils passed on", 3, { {}, nil, nil }, DEVICE_FOURTH }, + { "no parameters", 2, { nil, false }, "tParams should be a table" }, + { "a string of parameters", 1, { "" }, "tParams should be a table" }, + { "too few arguments", 0, {}, TOO_FEW }, + } + for _, case in ipairs(deviceCalls) do + local label, count, args, want = unpack(case, 1, 4) + T.eq( + "SendToDevice with " .. label, + refusal(function() + C4:SendToDevice(999999, "PROBE", unpack(args, 1, count)) + end), + want + ) + end + + local proxyCalls = { + { "a message type and a flag", 3, { {}, "COMMAND", true }, nil }, + { "a notify and a flag", 3, { {}, "NOTIFY", false }, nil }, + { "a string of parameters", 2, { "", "NOTIFY" }, nil }, + { "a nil message type before a flag", 3, { {}, nil, true }, CALL_TYPE }, + { "a flag in the message type's place", 2, { {}, true }, CALL_TYPE }, + { "a nil message type before a false flag", 3, { {}, nil, false }, CALL_TYPE }, + { "a nil message type passed on", 2, { {}, nil }, CALL_TYPE }, + { "a nil 5th passed on", 3, { {}, "COMMAND", nil }, FIFTH }, + { "a string 5th", 3, { {}, "COMMAND", "x" }, FIFTH }, + { "no parameters", 2, { nil, "NOTIFY" }, "Error: 3rd parameter must be a string (strValue) or table (tParams)" }, + { "no parameters passed on", 1, { nil }, "Error: 3rd parameter must be a string (strValue) or table (tParams)" }, + { "too few arguments", 0, {}, TOO_FEW }, + } + for _, case in ipairs(proxyCalls) do + local label, count, args, want = unpack(case, 1, 4) + T.eq( + "SendToProxy with " .. label, + refusal(function() + C4:SendToProxy(9999, "PROBE", unpack(args, 1, count)) + end), + want + ) + end +end + -------------------------------------------------------------------------------- T.section("Lua 5.1 library") -------------------------------------------------------------------------------- diff --git a/template/test/test_secret_sends_trace.lua b/template/test/test_secret_sends_trace.lua index 3dfc969..c620135 100644 --- a/template/test/test_secret_sends_trace.lua +++ b/template/test/test_secret_sends_trace.lua @@ -19,11 +19,14 @@ local CODE = "FAKE-CODE-0000" -- What reached C4, as each call handed it over. local sent = {} +local shimSendToProxy, shimSendToDevice = C4.SendToProxy, C4.SendToDevice function C4:SendToProxy(...) sent[#sent + 1] = { "SendToProxy", select("#", ...), ... } + return shimSendToProxy(self, ...) end function C4:SendToDevice(...) sent[#sent + 1] = { "SendToDevice", select("#", ...), ... } + return shimSendToDevice(self, ...) end --- Everything lib.logging prints at trace level while fn runs. @@ -57,13 +60,20 @@ sent = {} output = traced(function() SendSecretToDevice(42, "ARM", { UserCode = CODE }) end) -T.contains("its device and command", output, "C4:SendToDevice(42, ARM, ***REDACTED***, nil, false)") +T.contains("its device and command", output, "C4:SendToDevice(42, ARM, ***REDACTED***, false, false)") T.excludes("and never the code", output, CODE) T.eq("C4 is handed the parameters whole", sent[1][5].UserCode, CODE) -T.eq("and told not to log the command's content", { sent[1][2], sent[1][7] }, { 5, false }) +T.eq( + "and told not to log the command's content, with empty values refused", + { sent[1][2], sent[1][6], sent[1][7] }, + { 5, false, false } +) sent = {} SendSecretToDevice(42, "ARM", { UserCode = CODE }, nil, true) T.eq("unless the caller asks it to", sent[1][7], true) +sent = {} +SendSecretToDevice(42, "ARM", { UserCode = "" }, true) +T.eq("and empty values allowed when asked for", sent[1][6], true) T.section("the plain wrappers still trace every argument") output = traced(function() @@ -72,4 +82,42 @@ end) T.excludes("nothing is redacted", output, "***REDACTED***") T.contains("the call is traced", output, "C4:SendToProxy(5001, LOCK_STATUS_CHANGED, ") +T.section("an argument left out before one that is given reaches C4 as its default") +sent = {} +local calls = { + { + "SendSecretToDevice, with no flags", + function() + SendSecretToDevice(42, "ARM", { UserCode = CODE }) + end, + }, + { + "SendToDevice, told only not to log", + function() + SendToDevice(42, "REFRESH", {}, nil, false) + end, + }, + { + "SendSecretToProxy, told only to allow empty values", + function() + SendSecretToProxy(5001, "USER_ADDED", { PASSCODE = "" }, nil, true) + end, + }, + { + "SendToProxy, told only to allow empty values", + function() + SendToProxy(5001, "LOCK_STATUS_CHANGED", { LOCK_STATUS = "" }, nil, true) + end, + }, +} +for _, call in ipairs(calls) do + T.truthy("C4 accepts " .. call[1], pcall(call[2])) +end +T.eq("a device command's empty values refused", { sent[1][6], sent[2][6] }, { false, false }) +T.eq("a proxy message sent as a command", { sent[3][6], sent[4][6] }, { "COMMAND", "COMMAND" }) +sent = {} +SendToDevice(42, "REFRESH", {}) +SendToProxy(5001, "LOCK_STATUS_CHANGED", {}) +T.eq("and nothing is added when nothing follows", { sent[1][2], sent[2][2] }, { 3, 3 }) + T.finish()