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()