Third-party plugins
Ten, eight of them chosen on a scan of 984 real-world scripts.
Environment variables, used in 95 scanned scripts — the most-used third-party plugin here that is uniform all the way through. Source: https://github.com/GsNSIS/EnVar.
setHKCU and setHKLM are stateful: each applies to every later call in
the script, not to one. That is why they are ordinary calls rather than a
parameter on the others. setHKCU is the default.
| Method | Arguments | Returns |
|---|---|---|
.setHKCU() |
— | nothing |
.setHKLM() |
— | nothing |
.check(name, value) |
string, path |
code |
.addValue(name, value) |
string, path |
code |
.addValueEx(name, value) |
string, path |
code |
.setValue(name, value) |
string, path |
code |
.setValueEx(name, value) |
string, path |
code |
.deleteValue(name, value) |
string, path |
code |
.delete(name) |
string |
code |
.update(root, name) |
string, string |
code |
The code is a uint, and the five values are documented: 0 success, 1
cannot read, 2 no such variable, 3 no such value, 4 cannot write. uint
rather than int because the plugin’s own table is the proof — no code is
negative, and if code == 0 compiles to IntCmpU rather than a string compare.
Three things the table cannot say:
checkasks three questions."NULL"in the value position means “does this variable exist”;"NULL"in both means “is the environment writable at all”; neither means “does this variable contain this value”.- The
Exvariants writeREG_EXPAND_SZ, so a value containing%LOCALAPPDATA%is expanded on read rather than frozen on write.addValueExalso converts an existing plain variable to the expandable type — reach for it on a new variable, and leaveaddValuealone onPATH. update’s root is"HKCU","HKLM", or anything else meaning both (appended, HKLM first). The empty string is meaningful here rather than missing, which is why it is a plainstringand not an enum.updateignoressetHKCU/setHKLMand writes nothing — it reloads the variable into the running installer’s own environment, which is what a laterexecWaitneeds.
The value position is a path because that is what the plugin’s documentation
calls it and what the plugin is for. The cost is real: a variable whose
value is not a path gets its forward slashes turned into backslashes. Storing a
URL in an environment variable wants raw.
SimpleSC
Section titled “SimpleSC”Windows services, used in 61 scanned scripts. Source: https://nsis.sourceforge.io/NSIS_Simple_Service_Plugin.
| Method | Arguments | Returns |
|---|---|---|
.installService(name, display, type, start, binary, deps, account, password) |
string, string, int, int, string, string, string, string |
code |
.removeService(name) |
string |
code |
.startService(name, arguments, timeout) |
string, string, int |
code |
.stopService(name, waitForRelease, timeout) |
string, int, int |
code |
.existsService(name) |
string |
code |
.serviceIsRunning(name) |
string |
code, running |
.getServiceStatus(name) |
string |
code, status |
.setServiceDescription(name, text) |
string, string |
code |
.setServiceStartType(name, type) |
string, int |
code |
.setServiceFailure(name, reset, message, command, ×3 type and delay) |
ten, see the .toml |
code |
Every code is int, not uint: the documentation says 0 for success and “the
Windows error code” otherwise without ever bounding it below, and uint needs
proof rather than an absence of counterexamples.
serviceIsRunningandgetServiceStatuspush two. The code comes off first and only says whether the question could be asked; the answer is the second value.local queried, running = …— swapping those two names compiles, which is the whole reason the order is written down.existsServicereturns0for yes, which reads backwards. One scanned script carries the comment; <> 0 => service exists, which has it wrong.installService’s binary is astring, not apath, because it is a command line: a scanned script passes"$INSTDIR\bin\agent.exe -conf $\"$INSTDIR\conf\cli.conf$\""there, and normalising slashes would rewrite the arguments too.- Nothing here is optional. Three calls in the scanned scripts pass fewer arguments
than documented —
startService "$name"with no timeout, for instance. A plugin reads a fixed number of items off the stack, so a short call reads whatever the script happened to leave there. Those are bugs in two scripts, not evidence of an optional tail.
The twenty-odd further methods the page documents are left out because none is common:
no scanned script calls them. Each is five lines in your own
.toml.
SimpleSC.getErrorMessage is not declarable
Section titled “SimpleSC.getErrorMessage is not declarable”Both calls to it in the scanned scripts read:
Push $0SimpleSC::GetErrorMessagePop $0The error code goes in by Push, not as an inline argument. params become
the arguments written after Plugin::Method, so a .toml has no way to describe
it. Write those three lines as raw. The documentation lists it as taking a
parameter, which is why the scanned scripts decided it.
Nsis7z
Section titled “Nsis7z”7-Zip extraction, used in 5 scanned scripts. Source: https://nsis.sourceforge.io/Nsis7z_plug-in.
| Method | Arguments | Returns |
|---|---|---|
.extract(archive) |
path |
nothing |
.extractWithDetails(archive, template) |
path, string |
nothing |
Neither pushes anything at all — not a status, not an error. Unlike
nsisunz, which pushes "success" or an error sentence, this plugin is silent,
and an extraction that did nothing looks exactly like one that worked. Check the
result with fileExists on something the archive was supposed to contain.
outputs = [] is what makes local ok = nsis7z.extract(…) an error here rather
than a Pop that steals somebody else’s value.
The archive goes into whatever setOutPath last named — there is no destination
argument. extractWithDetails’s template has its %s replaced with each file
name, which is why it is a string rather than a path.
Two behaviours that are not arity, so they live here rather than in the
declaration: files whose name does not end in .7z are reportedly not
extracted, and SetOverwrite is not honoured.
extractWithCallback is not declarable.
HTTP and FTP transfer, used in 46 scanned scripts — the second-most-used third-party
plugin here, and the one with the most flags.
Source: Contrib/Inetc/inetc.cpp, checked against
https://nsis.sourceforge.io/Inetc_plug-in.
| Method | Arguments | Returns |
|---|---|---|
.get(url, file) |
string, path |
status (string) |
.head(url, file) |
string, path |
status (string) |
.put(url, file) |
string, path |
status (string) |
"OK" is success and every other value is an error sentence rather than a
code — "Terminated", "Cancelled", a WinInet message with its number spliced
in. So the test is a string comparison and the value is worth printing as it
stands.
head requests the headers only and writes the raw response to the file;
put uploads the local file to the URL, with the arguments in the same order —
the URL is still first.
/END is not optional here
Section titled “/END is not optional here”inetc.cpp:880 does not count arguments. It reads url/file pairs off the stack
in a loop and stops on /END:
while(!popstring(url) && lstrcmpi(url, TEXT("/end")) != 0){ if(popstring(fn) != 0 || lstrcmpi(url, TEXT("/end")) == 0) break;The plugin’s own wiki calls /END optional, “required if you stores other vars
in the stack” — which is a description of every call this compiler emits. A
plugin call sits between layout’s caller-saves, so the value under the last
argument is a live register: without the terminator, inetc would take it for a
third URL, pop again for its file name, and the restore afterwards would collect
whatever the loop left.
That is why it is terminator
in the declaration and not a flag. It is emitted on every call, and a call site
can neither leave it off nor write it.
Eighteen flags
Section titled “Eighteen flags”All leading, all order-free — inetc.cpp:1381 loops while(!popstring(url) && *url == TEXT('/')) and pushes the first non-switch token back — and every
valued one carries its value in a separate token.
| Flag | Value | What it does |
|---|---|---|
silent weaksecurity nocancel nocookies noproxy |
— | hide the UI; accept a bad certificate; lock Cancel; drop cookies; ignore IE’s proxy |
caption banner popup canceltext question |
string |
the four progress presentations and the confirm text |
proxy username password useragent header |
string |
connection settings; header is a raw request header |
connecttimeout receivetimeout |
uint |
seconds |
resume |
string |
retry prompt; "" accepts the default |
All three entry points take all eighteen, and the wiki says otherwise — its
put synopsis omits /RESUME, /QUESTION and /HEADER, and its head entry
is one sentence long. The source settles it the other way: put, head and
post each set a global and then call get, so there is one flag parser and
one flag set. The three declarations are identical apart from the method name.
local inetc = plugin "Inetc"local url = "https://example.com/toolchain.zip"
local status = inetc.get(url, PLUGINSDIR .. "/toolchain.zip", { caption = "Fetching the toolchain", silent = true, connecttimeout = 30,})-- inetc::get /CONNECTTIMEOUT 30 /SILENT /CAPTION "Fetching the toolchain" … /ENDNote the emitted order: Inetc.toml lists the flags in the order inetc.cpp
checks them, and the call site’s order is discarded. It has to be — the plugin
accepts them in any order, and two calls naming the same three must not emit two
different lines.
/TRANSLATE stays out for the reason it stays out of NSISdl: it carries eight
or nine further positional strings. /TOSTACK and /TOSTACKCONV stay out for a
new one — they push the downloaded body underneath the status, so a flag would
be changing outputs, and outputs is the one thing a declaration is for.
post is not declarable.
NScurl
Section titled “NScurl”libcurl in a plugin — HTTP/HTTPS/FTP with TLS, HTTP/2 and HTTP/3, a background
queue, and three hash helpers.
Source: https://github.com/negrutiu/nsis-nscurl, src/nscurl/main.c for the
exports and curl.c / gui.c for the parameter loops.
Newer than the scan, and declared because its counts are hard to guess: eleven exports, each with its own push count, and no two alike.
| Method | Arguments | Returns |
|---|---|---|
.http(method, url, file) |
string, string, path |
status (string) |
.wait() |
— | — |
.query(keywords) |
string |
expansion (string) |
.cancel() |
— | — |
.md5(file) .sha1(file) .sha256(file) |
path |
hex digest (string) |
.escape(s) .unescape(s) |
string |
percent-coded (string) |
http pushes exactly one value on every path — GuiWait’s result when the
request was queued, a formatted Win32 message when a parameter was rejected
(main.c:290). By default that value is "OK" or an error sentence, and the
returns flag replaces it with any @KEYWORD@ expansion, so the comparison a
call site writes depends on the flag it set.
wait and cancel push nothing. wait computes the same string http
would push and drops it (main.c:388), which is why /RETURN is not declared
there — it formats a value nobody can read.
The third argument to http is the output file, or the literal "Memory" to
keep the body in the queue for query to read back.
The flags come after the arguments here
Section titled “The flags come after the arguments here”CurlParseRequestParam (curl.c:369) takes parameter 0 for the HTTP method, 1
for the URL and 2 for the output path — by position, not by content — and only
starts matching /SWITCH tokens from the fourth. A leading /SILENT is
therefore not a flag: it is the verb, and the request goes out asking a server
for SILENT.
That is the whole of trailing = true in
NScurl.toml. The
call site is unchanged — the table is still written last and still unordered —
and only the run of flags lands on the other side of the arguments.
/END is not optional for the same reason it is
not optional on inetc: main.c:238 reads parameters in a loop until it pops
it, and what lies under the last argument is layout’s caller-saves.
Thirty-four flags on http, twelve on wait
Section titled “Thirty-four flags on http, twelve on wait”The first block below is the request (curl.c), the second the progress UI
(gui.c) — http takes both, wait and cancel only what their own loops
match.
| Flag | Value | What it does |
|---|---|---|
resume insist noredirect encoding markoftheweb |
— | continue a partial file; retry on failure; don’t follow 3xx; accept compression; write the Zone.Identifier stream |
http11 http3 |
— | pin the protocol version; the last one written wins |
header useragent referer data |
string |
request headers (\r\n separates several), agent, referrer, request body |
connecttimeout completetimeout |
string |
a duration with a unit — "30s", "2m" — or bare milliseconds |
speedcap depend |
uint |
bytes/second ceiling; queue id this one waits on |
security castore |
string |
"weak"/"strong"; "true"/"false" for the Windows CA store |
cacert |
path |
a bundle, or "builtin" or "none" |
cert |
string |
a pinned SHA-1 fingerprint or a PEM blob |
proxy doh |
string |
proxy URL; DNS-over-HTTPS resolver |
cookiejar debug |
path |
cookie file; trace log |
tag |
string |
names this request, for wait, query and cancel |
returns |
string |
the @KEYWORD@ string http pushes instead of the status |
background page popup silent cancel |
— | return immediately; and the four presentations |
titlewnd textwnd progresswnd cancelwnd |
handle |
drive controls of your own — the handles a page.custom hands back |
id (wait, query, cancel) |
uint |
select one queued request |
remove (cancel) |
— | abort and drop it from the queue |
local nscurl = plugin "NScurl"local url = "https://example.com/tool.zip"
local status = nscurl.http("GET", url, PLUGINSDIR .. "/tool.zip", { connecttimeout = "30s", tag = "toolchain", silent = true,})-- NScurl::http "GET" "…" "$PLUGINSDIR\tool.zip" /CONNECTTIMEOUT "30s" /TAG "toolchain" /SILENT /ENDWhat stays out, and why it is the same reason twice
Section titled “What stays out, and why it is the same reason twice”/POST, /AUTH, /PROXYAUTH, /TLSAUTH, /LOWSPEEDLIMIT and /STRING all
pop two or more values after the switch — /TLSAUTH user pass,
/LOWSPEEDLIMIT bps duration, /POST [filename=…] [type=…] name data — and a
separate flag carries exactly one. Declaring one would emit the switch and a
single token, and the plugin would take the next argument as its second value.
There is no encoding for a multi-value flag, so there is no half-right version
of these to ship.
echo is out because of what it is: a debugging export that pops however many
strings you pushed. enumerate is out for the arity — it pushes one queue id
per matching request under an empty-string sentinel, and tagged decides
between two fixed counts, not between n of them.
Two single-value flags carry a hazard worth naming, because the plugin resolves
it at runtime and the compiler cannot. /DATA and /DEBUG each peek at the
token they pop and pop again if it was a keyword — -file, -string,
-memory, nodata. A call site passing one of those words as the value would
unbalance the stack. The keyword forms are not reachable from here, and passing
a keyword by accident is the one way to reach them.
The hash helpers take path rather than string for the same class of reason.
IDataParseParam (utils.c:1008) guesses between a file and a literal by
asking whether the path exists, and a $PLUGINSDIR/tool.zip written the way
every other path in a source is written does not exist when written that way — it
would silently hash the text. Declaring the position a path normalises the
separators first. Hashing a literal string with a / in it is what that costs.
SimpleFC
Section titled “SimpleFC”NSIS Simple Firewall, used in 24 scanned scripts. It drives INetFwPolicy2, so it reaches
per-profile rules, direction, ports and ICMP types that
nsisFirewallW cannot express, and it ships an ANSI and a Unicode build, both at 1.21, both named
SimpleFC.dll. Source: Source/SimpleFC.dpr, and it is the first of these read from
Delphi rather than C++ — the idiom is PopString / PushString from
nsis.pas, but the counting is the same.
All 33 exported methods are declared. Every one of them is straight-line: not a
single PushString in the file sits inside a branch, which is why a plugin this
wide needs no tagged entry at all.
The status is 0, and it comes off first
Section titled “The status is 0, and it comes off first”The plugin’s own ResultToStr is the whole vocabulary:
function ResultToStr(Value: Boolean): String;begin if Value then result := '0' else result := '1';end;0 is success and 1 is failure — the opposite of the C convention and the
same as SimpleSC’s. The readers push their answer first and the status last,
so the status pops first and the answers follow it:
PushString(BoolToStr(Allowed));PushString(BoolToStr(Restricted));PushString(FirewallResult); // popped firstThat gives isIcmpTypeAllowed three outputs — status, allowed, restricted — and
it is the widest fixed arity in any declaration here.
Three places the readme is wrong
Section titled “Three places the readme is wrong”Measured by diffing every synopsis and every worked example in Readme.txt
against the pops and pushes in SimpleFC.dpr. Three disagreements, all in the
same direction — the prose under-counts:
| Method | Readme says | The DLL does |
|---|---|---|
enableDisableNotifications |
one Pop in one example, none in the other |
pushes two: the status, and an echo of the argument beneath it |
enableDisablePort |
synopsis [port] [protocol] |
pops three — the synopsis omits the [status] its own examples pass |
enableDisableApplication |
synopsis [path] |
pops two, same omission |
The first is the costly one: both of the readme’s own examples leak a value, so
a script that follows them has a stack one item deep for the rest of the
section. The second push looks like a copy-paste of the reader’s — every other
setter (enableDisableFirewall, startStopFirewallService,
allowDisallowExceptionsNotAllowed) pushes one — but what the DLL does is the
arity, whatever the author meant.
This is the fourth plugin where the source and the documentation disagree, and
the fourth time the source won. The rule stated under
AccessControl needs no restating: documentation describes
the path its author was thinking about, and an arity is a claim about every
path.
Arguments are codes, not names
Section titled “Arguments are codes, not names”Most parameters are int because the plugin takes Windows’ own enumerations
raw: protocol is 6 for TCP and 17 for UDP, scope is 0 for all networks,
IP version, profile, direction and action likewise. The readme’s Parameters
list is the mapping and is correct there. advAddRule takes fifteen of them in
one call — the longest params in any declaration, and the one place where
reading the emitted line back is easier than reading the Lua.
nsProcess
Section titled “nsProcess”Process control, used in 25 scanned scripts. The original worked example, and the only one of these read from the plugin’s own source. Source: https://nsis.sourceforge.io/NsProcess_plugin.
| Method | Arguments | Returns |
|---|---|---|
.findProcess(name) |
string |
code (uint) |
.killProcess(name) |
string |
code (uint) |
.closeProcess(name) |
string |
code (uint) |
0 found, 603 not running, and a documented list of failures above 600 —
non-negative throughout, which is what earns uint.
closeProcess closes the process’s windows and waits a few seconds before
terminating it. That is the difference from killProcess and the reason to
prefer it.
_Unload is deliberately absent: it exists so a script using /NOUNLOAD can
release the DLL, and this declaration has no nounload, so the DLL is already
freed after every call.
The Unicode build ships as nsProcessW.dll, and version 1.6’s readme says to
rename it to nsProcess.dll. Do that: an Installua installer is always Unicode,
and the declaration calls nsProcess::, so it loads whichever DLL has that name.
nsisFirewallW
Section titled “nsisFirewallW”nsisFirewall 1.2, used in 1 scanned script: add a program to the Windows Firewall
exception list, or take it off again.
Source: nsisFirewall.cpp, from http://wiz0u.free.fr/prog/nsisFirewall/.
| Method | Arguments | Returns |
|---|---|---|
.addAuthorizedApplication(path, name) |
path, string |
HRESULT (int) |
.removeAuthorizedApplication(path) |
path |
HRESULT (int) |
Declared under its Unicode name. Version 1.2 added Unicode as a DLL of its
own, nsisFirewallW.dll, and NSIS finds a plugin’s DLL by the name in front of
the :: — so the Unicode build is called as nsisFirewallW::…, and that is the
name this declaration has. The ANSI nsisFirewall cannot load in an Installua
installer at all.
0 is success and anything below it is an HRESULT, which is why the code is
int rather than uint. name is the title the rule gets in the firewall’s
control panel. The plugin uses the older INetFwMgr interface and knows nothing
of firewall profiles; SimpleFC is the one for anything more.
WAInfo
Section titled “WAInfo”Winamp’s directories, version, default plugins and skin, and the running process — for installing a Winamp plugin, skin or language pack into the right place. Not chosen on the scan: it ships because Winamp and NSIS come from the same house. Source: none. WAInfo 2.1 is freeware without source, from https://www.pawelporwisz.pl/nsis/plugins/WAInfo/WAInfo.php.
| Method | Arguments | Returns |
|---|---|---|
.getWinampInformation(dir, what) |
path, string |
a path, 0/1, or "error" |
.getWinampVersionInfo(dir, what) |
path, string |
the field, or "error" |
.getWinampPluginInformation(dir, kind, what) |
path, string, string |
the field, or "error" |
.getWinampComponentInformation(dir, what) |
path, string |
the name, or "error" |
.setWinampDefaultComponentName(dir, kind, name) |
path, string, string |
"0" success, "1" failure, or "error" |
.locateWinampRunningProcess(dir) |
path |
1 running, 0 not (uint) |
.closeWinampRunningProcess(dir) |
path |
0 closed, 1 failed (uint) |
.getPluginInfo() |
— | the plugin’s own name and version |
Read from the DLL, since there is no source to read. Each export makes
exactly one call to the Delphi PushString and no branch skips it, so every
method pushes one value on every path. Its PopString calls match the argument
counts on the author’s page. "error" is that one value, never a second one
underneath, which is why none of these needs tagged.
getPluginInfo is not on the author’s page at all; only the bundled
WAInfoDemo.nsi calls it.
dir is the Winamp installation directory, and "" means the one Winamp’s
uninstall key points at. A portable Winamp is passed by its path. what and
kind are the author’s keywords, such as "PluginsDir", "VISDir", "VIS",
"Skin" or "VersionMajor", and they are copied into the line as written.
setWinampDefaultComponentName returns "0" on success and "1" on failure.
The author’s prose has these swapped, which is a typo: every one of the
examples tests "0" as success. It stays string because "error" is a
third answer.
The archive has Win32/Release/WAInfo.dll and Win64/Release/WAInfo.dll, both
Unicode. Put the one matching your makensis target into
Plugins/x86-unicode or Plugins/amd64-unicode, or add a dir in a project
copy of the declaration.
local waInfo = plugin "WAInfo"
local visDir = waInfo.getWinampInformation("", "VISDir")if waInfo.locateWinampRunningProcess("") == 1 then waInfo.closeWinampRunningProcess("")endAccessControl
Section titled “AccessControl”ACLs, used in 111 scanned scripts — the most-used third-party plugin in the scan, and all twenty-five of its methods are declared.
Source: AccessControl.cpp. This is the plugin where the source had to
settle it, twice. The readme and the wiki page are both wrong about the stack in
opposite directions, and either on its own leads to a different — wrong — set of
declarations. Twenty-three of the twenty-five are declarable only because the
format has tagged, and the remaining two were misread here
until the source was read a second time.
Every method’s arity
Section titled “Every method’s arity”Twenty-two of them behave the same way. The mutators and the object readers route
every diagnosed failure through ABORT_s/ABORT_d, which push a description
and then jump to a cleanup that pushes "error" on top of it:
#define ABORT_s(x, y) { showerror_s(TEXT(x), y); goto cleanup; }...if (ret) pushstring(TEXT("error"));So GrantOnFile, SetOnFile, DenyOnFile, RevokeOnFile, ClearOnFile,
SetFileOwner, SetFileGroup, the inheritance pair, the Get*Owner /
Get*Group readers and the whole *OnRegKey family push "ok" alone, or
"error" and a description underneath it — which is exactly tagged = ["error"], more = ["string"].
| Method | Arguments | Returns |
|---|---|---|
.enableFileInheritance(path) |
path |
"ok", or "error" + why |
.disableFileInheritance(path) |
path |
the same |
.grantOnFile(path, trustee, permissions) |
path, string, string |
the same |
.setOnFile(…) / .denyOnFile(…) / .revokeOnFile(…) / .clearOnFile(…) |
the same three | the same |
.setFileOwner(path, trustee) |
path, string |
the same |
.setFileGroup(path, trustee) |
path, string |
the same |
.getFileOwner(path) |
path |
the owner, or "error" + why |
.getFileGroup(path) |
path |
the group, or "error" + why |
.enableRegKeyInheritance(root, key) |
string, string |
"ok", or "error" + why |
.disableRegKeyInheritance(root, key) |
string, string |
the same |
.grantOnRegKey(root, key, trustee, permissions) |
four string |
the same |
.setOnRegKey(…) / .denyOnRegKey(…) / .revokeOnRegKey(…) / .clearOnRegKey(…) |
the same four | the same |
.setRegKeyOwner(root, key, trustee) |
three string |
the same |
.setRegKeyGroup(root, key, trustee) |
three string |
the same |
.getRegKeyOwner(root, key) |
string, string |
the owner, or "error" + why |
.getRegKeyGroup(root, key) |
string, string |
the group, or "error" + why |
.nameToSid(name) |
string |
the SID, or "error" + why |
.sidToName(sid) |
string |
domain, then name — always two |
.getCurrentUserName() |
— | user name, always one |
Note that getFileOwner’s success value is the payload rather than a tag: only
the failure side is a fixed string, which is why tagged is a list of
first-values to test and never a tag to parse.
The wiki’s examples all pop once:
AccessControl::SetFileOwner "C:\test.txt" "Waterloo\Mathias"Pop $0 ; "error" on errorsThat is a happy-path example, and on the error path it leaks the description onto the stack. The scanned scripts have the correct idiom, and it is the one Installua now emits:
AccessControl::GrantOnFile "$INSTDIR" "(BU)" "FullAccess"Pop $R0${If} $R0 == error Pop $R0${EndIf}The three SID helpers, read twice
Section titled “The three SID helpers, read twice”These use no ABORT at all — each has a hand-rolled error path — and an
earlier version of this page got two of them wrong, in a way worth
recording because it is the same mistake the readme makes. NameToSid and
SidToName both end with the file’s standard trailing line:
if (ret) pushstring(TEXT("error"));Reading only the body of each function suggests the failure path pushes a
message and stops. It does not: ret is still 1 there, so "error" goes on
top of the message and both come back.
| Success | Failed lookup | Allocation failure | |
|---|---|---|---|
GetCurrentUserName |
1 (name) | 1 (empty string) | 1 ("error") |
NameToSid |
1 (SID) | 2 ("error", message) |
1 ("error") |
SidToName |
2 (name, domain) | 2 ("error", message) |
1 ("error") |
So NameToSid is tagged like the other twenty-two, and SidToName is
uniform at two — the arity does not vary at all, and the pair is
(domain, name) on success and ("error", message) on failure. The readme
presents it as Pop $Domain / Pop $Username, which is right about the success
path and silent about the other; both calls in the scanned scripts pop twice because both
assume the readme, and both are correct by accident.
getCurrentUserName is the uniform one. It gives the bare account
name without a domain (GetUserName, not GetUserNameEx) and never checks the
result, so a failed lookup pushes an empty string rather than a sentinel — test
emptiness, not equality.
The two flags, and the eleven methods that ignore them
Section titled “The two flags, and the eleven methods that ignore them”/noinherit and /sid are parsed by PopFileArgs and PopRegKeyArgs, which
every method but the three SID helpers goes through — so all 22 accept both.
They are declared on the 14 that act on one:
| Flag | Declared on | Read by |
|---|---|---|
noinherit |
grant, set, deny, revoke, clear — on file and reg key |
ChangeDACL, ClearACL |
sid |
getFileOwner, getFileGroup, getRegKeyOwner, getRegKeyGroup |
GetOwner |
local accessControl = plugin "AccessControl"
accessControl.grantOnFile(INSTDIR, "(BU)", "FullAccess", { noinherit = true })-- AccessControl::GrantOnFile /noinherit $INSTDIR "(BU)" "FullAccess"The other eleven behave differently. setFileOwner and setFileGroup
reach ChangeOwner, which reads neither. enableFileInheritance and
disableFileInheritance are worse than that: ChangeInheritance does read
noInherit, but the dispatcher overwrites it with the enable-or-disable choice
one line before the call, so a /noinherit on those two is parsed, stored and
discarded. Declaring it there would have been a flag that compiles, assembles
and does nothing — which is the same class of mistake as a wrong output count,
and caught only by reading AccessControl.cpp.