Ware: Reference Manual

Table of Contents

1 – Introduction

Ware is an embeddable library that allows addons to transfer data into (and out of) restricted environments via Lua's ordinary table syntax, and allows passing arguments to restricted snippets executed from insecure code. Essentially, it provides drop-in functionality-extending replacements for FrameXML's GetManagedEnvironment and SecureHandlerExecute APIs, as well as some support functions for interacting with restricted tables from insecure code.

Ware can be useful for addons that provide user-configurable in-combat functionality using the Restricted Environment or Secure Handlers APIs.

2 – Transferring data into restricted environments

Aside from using Ware, there are three other options for transferring user data into the restricted environment you could consider.

It is possible to serialize the data into an executable Lua snippet and execute that snippet via SecureHandlerExecute. Restricted execution infrastructure requires that such snippets to not contain curly brackets or the character sequence "function", so some non-standard escaping of user-supplied strings can be necessary. Additionally, Lua limitations can make creating snippets at runtime error-prone: for example, the maximum number of arguments to the newtable(...) function is limited, requiring long array initialization to be split into multiple statements.

It is possible to store simple user configuration entirely in frame attributes, accessing it via e.g. owner:GetAttribute("setting-name") each time a snippet needs to take a user preference into account. Attributes are a flat namespace with case-insensitive string keys (that are coerced into lower case when passed to the OnAttributeChanged script handler), which can be inconvenient for more complex data.

Finally, it is possible to write a SecureHandler delegate (e.g. by wrapping the OnAttributeChanged script handler of a dedicated frame using the owner of the target restricted environment as the wrap header) responding to attribute changes to update values stored within a restricted environment. This enables more complex data structures to be updated without requiring code generation at runtime, but requires the delegate performing such updates to be maintained in parallel with the code requesting them.

Ware uses the third approach internally in order to provide "handles" to restricted tables that can be written to (out of lockdown) using Lua's normal table access syntax, simplifying both the restricted environment snippets and insecure code communicating with them.

3 – How to use Ware

The following example illustrates some of what you can use Ware for: it sets up a SecureHandler Button widget that hides other action buttons when clicked, or shows them all if clicked with a special configurable button. You can then use /click RandomActionButtonHide and /click RandomActionButtonHide ShowAll to perform those actions in combat.

Ware example
  1. -- Prepare a SecureHandler button
  2. local owner = CreateFrame("Button", "RandomActionButtonHide", nil, "SecureHandlerClickTemplate")
  3. owner:SetAttribute("_onclick", [[
  4. local randIndex = math.random(#buttons)
  5. for i=1, #buttons do
  6. buttons[i]:SetShown(button == SHOW_ALL or i == randIndex)
  7. end
  8. ]])
  9. -- Ware is in the addon private table if embedded, or in _G if installed as an addon
  10. local _, T = ...
  11. local Ware = T.Ware or _G.Ware
  12. -- Transfer data and frame references into the restricted environment
  13. local h = Ware.GetRestrictedEnvironment(owner)
  14. h.SHOW_ALL = "ShowAll"
  15. local hButtons = Ware.newtable(h, "buttons")
  16. for i=1, 12 do
  17. hButtons[i] = _G['ActionButton' .. i]
  18. end

3.1 – Accessing and distributing Ware

If your addon requires Ware to function, embed Ware in your addon by including the Ware.lua file in your .toc, and access Ware via the Ware key on the private addon table (e.g. local _, T = ...; local Ware = T.Ware). If desired, you can download the latest version of the Ware library using git.

If Ware is used as an optional or a developer-only tool, you can instead list Ware as an optional dependency, instruct interested parties to download the Ware addon, and access Ware via the Ware global variable. Do not redistribute Ware as a stand-alone addon (i.e. with its own toc file), as multiple addons doing this can cause unexpected version downgrades.

3.2 – Using Ware handles

Ware handles are light userdata proxies for restricted tables, keeping track of the environment the table is associated with and allowing the table to be written to from insecure code while outside lockdown. You can acquire a handle for the environment of a SecureHandlers header (or any protected frame) by calling Ware.GetRestrictedEnvironment.

Given a Ware handle h, you can:

  • Index it using the h[key] syntax as you would a regular table. If key is a widget table, it is transparently mapped to a frame handle. The returned value is transparently mapped to a Ware handle (if originally a restricted table) or to a widget table (if originally a frame handle).
  • Get the length (per Lua's length operator semantics) of the backing restricted table using #h.
  • Get an iterator triple for use in a for loop with Ware.pairs or by calling h(0).
    When iterating over a Ware handle, the keys returned retain their original types (i.e. frame handles are returned as frame handles), while the values are mapped as they would be if accessed via h[key], with frame handles mapped to widget tables and restricted tables mapped to Ware handles.
  • Outside combat lockdown, write into it using the ordinary h[key] = value syntax.
    If key is not a boolean, string, number, frame handle, nor a widget table (transparently mapped to a frame handle), an error is thrown.
    If value is not nil, nor a boolean, string, number, frame handle, widget table (transparently mapped to a frame handle) value, a Ware handle from the same restricted environment as h, or the original Ware.newtable function, an error is thrown.
    If value is the original Ware.newtable function, a new restricted table is created instead. This is convenient for creating empty tables you do not intend to immediately write to; otherwise, calling Ware.newtable allows array initialization and provides you with a reference to the created table immediately.
    You must not attempt to write into a Ware handle while combat lockdown is in effect (i.e. between PLAYER_REGEN_DISABLED and PLAYER_REGEN_ENABLED events, while InCombatLockdown() returns true).

To conserve resources, Ware handles to restricted tables within an environment are first made writable when you attempt to write to them. In most cases, this should be transparent to your code; however, if you acquire a Ware handle, then move the associated restricted table (or one of its ancestors) to a different location within an environment, a "lost handle" error may occur when you attempt a write. This error indicates that Ware is no longer able to locate the backing restricted table within its environment, and can therefore not modify its contents. You can avoid this by calling Ware.PrepareWritableHandle at the earliest opportunity after acquiring any Ware handle you intend to write to after rearranging restricted table structures.

3.3 – Snippets with arguments and returns

Ware provides three functions that extend the functionality of SecureHandlerExecute by allowing you to pass arguments to, and receive return values from executed restricted snippets. Only simple nil, boolean, number or string values can be passed as arguments or returned via this mechanism; other values are transformed to nil (i.e. both arguments and return values are pass through scrub).

  • Ware.Run is roughly equivalent to frame:Run within the restricted environment.
  • Ware.RunFor is roughly equivalent to frame:RunFor within the restricted environment.
  • Ware.RunAttribute is roughly equivalent to frame:RunAttribute within the restricted environment.

4 – API reference

Ware.GetRestrictedEnvironment(owner)

Given an explicitly protected widget owner, returns a Ware handle to its restricted environment, and the restricted table backing it. If owner does not have an already-initialized restricted environment and combat lockdown is in effect, only the Ware handle is returned.

Ware.GetBackingRestrictedTable(handle)

Given a Ware handle, returns the restricted table backing it.

If called in combat with a Ware handle to a not-yet-initialized restricted environment, returns nil instead.

Ware.IsWareHandle(handle)

Returns true if the argument is a Ware handle and false otherwise.

Ware.PrepareWritableHandle(handle)

Ensures that a given Ware handle can be written into, returning the same handle if successful (or redundant) and nil otherwise.

This function cannot succeed while in combat lockdown.

Ware.newtable(handle, key, ...)

Creates a new restricted table within a given Ware handle's restricted environment, returning a Ware handle to the created restricted table.

If key is not nil, additionally assigns handle[key] = nt, where nt is the Ware handle returned by this function. If key is neither nil nor a boolean, number, string, frame handle, nor a widget table (transparently mapped to a frame handle), an error is thrown.

Any values in the vararg expression are used to initialize the array portion of the newly-created table. If any value in the vararg expression is not nil nor a boolean, number, string, frame handle, widget handle (transparently mapped to a frame handle) nor a Ware handle within the same environment as handle, an error is thrown.

You must not call this function while in combat lockdown.

Ware.Run(owner, "body", ...)

Executes a restricted snippet body in the environment of a given owner widget (must be explicitly protected) or a Ware handle returned by Ware.GetRestrictedEnvironment. The vararg expression is passed (scrub'ed) to the snippet, and the snippet's scrub'ed return values are returned.

You must not call this function while in combat lockdown.

Ware.RunFor(owner, otherFrame, "body", ...)

Executes a restricted snippet body in the environment of a given owner widget (must be explicitly protected) or a Ware handle returned by Ware.GetRestrictedEnvironment. The frame handle (or widget table, transparently mapped to a frame handle) runFor passed as the self argument to the snippet. The vararg expression is passed (scrub'ed) to the snippet, and the snippet's scrub'ed return values are returned.

You must not call this function while in combat lockdown.

Ware.RunAttribute(owner, "attribute", ...)

Executes a restricted snippet stored in the specified attribute of the environment owner; owner must be an explicitly protected widget or a Ware handle returned by Ware.GetRestrictedEnvironment. The vararg expression is passed (scrub'ed) to the snippet, and the snippet's scrub'ed return values are returned.

You must not call this function while in combat lockdown.

Ware.pairs(h)

pairs-equivalent allowing h to be a Ware handle, restricted table, or a regular table.

Values returned by the Ware handle iterator returned by this function are mapped to Ware handles (if originally restricted tables) or widget tables (if originally frame handles); keys retain their original types.

Ware.next(h, k)

next-equivalent allowing h to be a Ware handle, restricted table, or a regular table.

If h is a Ware handle,

  • k is transparently mapped to a frame handle (if originally a widget table);
  • values returned by this function are mapped to Ware handles (if originally restricted tables) or widget tables (if originally frame handles);
  • keys returned by this function retain their original types.

Ware.unpack(t [, i [, j]])

unpack-equivalent, allowing t to be any indexable value (and such that, if no j is provided, #t returns a number).

If t is a Ware handle, any frame handle values in the unpacked array segment are returned mapped to widget tables.

Ware.wipe(h)

wipe-equivalent allowing h to be a Ware handle, restricted table, or a regular table.

You must not call this function on a Ware handle while in combat lockdown.

You should not call this function on restricted tables: it defers to rtable.wipe, which produces an error for all insecure callers.