Compare commits

..

6 Commits

Author SHA1 Message Date
tequ
63096d5fbc add test 2026-01-21 12:45:05 +09:00
tequ
2e128acdcf add execution test 2026-01-21 10:52:31 +09:00
tequ
043c60b62e Update new tests 2026-01-20 20:07:26 +09:00
tequ
5dd1198e4f Merge commit '5d9071695a616e1af378142e09649abc7d0e8afa' into hook-helper-func 2026-01-20 15:46:00 +09:00
tequ
5d9071695a Add tests for Hooks fee 2026-01-20 12:12:45 +09:00
tequ
ec6dc93834 Allow helper functions at Hooks 2026-01-19 16:44:23 +09:00
16 changed files with 2221 additions and 445 deletions

2
.gitignore vendored
View File

@@ -121,3 +121,5 @@ CMakeUserPresets.json
bld.rippled/
generated
guard_checker
guard_checker.dSYM

View File

@@ -192,7 +192,6 @@
#define sfNFTokenMinter ((8U << 16U) + 9U)
#define sfEmitCallback ((8U << 16U) + 10U)
#define sfHookAccount ((8U << 16U) + 16U)
#define sfHookAdministrator ((8U << 16U) + 98U)
#define sfInform ((8U << 16U) + 99U)
#define sfIndexes ((19U << 16U) + 1U)
#define sfHashes ((19U << 16U) + 2U)

View File

@@ -5,6 +5,7 @@
#include <memory>
#include <optional>
#include <ostream>
#include <set>
#include <stack>
#include <string>
#include <string_view>
@@ -282,7 +283,8 @@ check_guard(
* might have unforeseen consequences, without also rolling back further
* changes that are fine.
*/
uint64_t rulesVersion = 0
uint64_t rulesVersion = 0,
std::set<int>* out_callees = nullptr
)
{
@@ -492,17 +494,27 @@ check_guard(
{
REQUIRE(1);
uint64_t callee_idx = LEB();
// disallow calling of user defined functions inside a hook
// record user-defined function calls if tracking is enabled
if (callee_idx > last_import_idx)
{
GUARDLOG(hook::log::CALL_ILLEGAL)
<< "GuardCheck "
<< "Hook calls a function outside of the whitelisted "
"imports "
<< "codesec: " << codesec << " hook byte offset: " << i
<< "\n";
if (out_callees != nullptr)
{
// record the callee for call graph analysis
out_callees->insert(callee_idx);
}
else
{
// if not tracking, maintain original behavior: reject
GUARDLOG(hook::log::CALL_ILLEGAL)
<< "GuardCheck "
<< "Hook calls a function outside of the whitelisted "
"imports "
<< "codesec: " << codesec << " hook byte offset: " << i
<< "\n";
return {};
return {};
}
}
// enforce guard call limit
@@ -837,6 +849,42 @@ validateGuards(
*/
uint64_t rulesVersion = 0)
{
// Structure to track function call graph information
struct FunctionInfo
{
int func_idx;
std::set<int> callees; // functions this function calls
std::set<int> callers; // functions that call this function
bool has_loops; // whether this function contains loops
uint64_t local_wce; // local worst-case execution count
uint64_t total_wce; // total WCE including callees
bool wce_calculated; // whether total_wce has been computed
bool in_calculation; // for cycle detection in WCE calculation
FunctionInfo()
: func_idx(-1)
, has_loops(false)
, local_wce(0)
, total_wce(0)
, wce_calculated(false)
, in_calculation(false)
{
}
FunctionInfo(int idx, uint64_t local_wce_val, bool has_loops_val)
: func_idx(idx)
, has_loops(has_loops_val)
, local_wce(local_wce_val)
, total_wce(0)
, wce_calculated(false)
, in_calculation(false)
{
}
};
// Call graph: maps function index to its information
std::map<int, FunctionInfo> call_graph;
uint64_t byteCount = wasm.size();
// 63 bytes is the smallest possible valid hook wasm
@@ -1176,6 +1224,12 @@ validateGuards(
if (DEBUG_GUARD)
printf("Function map: func %d -> type %d\n", j, type_idx);
func_type_map[j] = type_idx;
// Step 4: Initialize FunctionInfo for each user-defined
// function func_idx starts from last_import_number + 1
int actual_func_idx = last_import_number + 1 + j;
call_graph[actual_func_idx] = FunctionInfo();
call_graph[actual_func_idx].func_idx = actual_func_idx;
}
}
@@ -1217,9 +1271,6 @@ validateGuards(
return {};
}
int64_t maxInstrCountHook = 0;
int64_t maxInstrCountCbak = 0;
// second pass... where we check all the guard function calls follow the
// guard rules minimal other validation in this pass because first pass
// caught most of it
@@ -1253,6 +1304,7 @@ validateGuards(
std::optional<
std::reference_wrapper<std::vector<uint8_t> const>>
first_signature;
bool helper_function = false;
if (auto const& usage = import_type_map.find(j);
usage != import_type_map.end())
{
@@ -1288,7 +1340,7 @@ validateGuards(
}
}
}
else if (j == hook_type_idx)
else if (j == hook_type_idx) // hook() or cbak() function type
{
// pass
}
@@ -1301,7 +1353,8 @@ validateGuards(
<< "Codesec: " << section_type << " "
<< "Local: " << j << " "
<< "Offset: " << i << "\n";
return {};
// return {};
helper_function = true;
}
int param_count = parseLeb128(wasm, i, &i);
@@ -1318,12 +1371,19 @@ validateGuards(
return {};
}
}
else if (helper_function)
{
// pass
}
else if (param_count != (*first_signature).get().size() - 1)
{
GUARDLOG(hook::log::FUNC_TYPE_INVALID)
<< "Malformed transaction. "
<< "Hook API: " << *first_name
<< " has the wrong number of parameters.\n";
<< " has the wrong number of parameters.\n"
<< "param_count: " << param_count << " "
<< "first_signature: "
<< (*first_signature).get().size() - 1 << "\n";
return {};
}
@@ -1370,6 +1430,10 @@ validateGuards(
return {};
}
}
else if (helper_function)
{
// pass
}
else if ((*first_signature).get()[k + 1] != param_type)
{
GUARDLOG(hook::log::FUNC_PARAM_INVALID)
@@ -1446,6 +1510,10 @@ validateGuards(
return {};
}
}
else if (helper_function)
{
// pass
}
else if ((*first_signature).get()[0] != result_type)
{
GUARDLOG(hook::log::FUNC_RETURN_INVALID)
@@ -1497,6 +1565,17 @@ validateGuards(
// execution to here means we are up to the actual expr for the
// codesec/function
// Step 5: Calculate actual function index and prepare callees
// tracking
int actual_func_idx = last_import_number + 1 + j;
std::set<int>* out_callees_ptr = nullptr;
// Only track callees if this function is in the call_graph
if (call_graph.find(actual_func_idx) != call_graph.end())
{
out_callees_ptr = &call_graph[actual_func_idx].callees;
}
auto valid = check_guard(
wasm,
j,
@@ -1506,33 +1585,188 @@ validateGuards(
last_import_number,
guardLog,
guardLogAccStr,
rulesVersion);
rulesVersion,
out_callees_ptr);
if (!valid)
return {};
if (hook_func_idx && *hook_func_idx == j)
maxInstrCountHook = *valid;
else if (cbak_func_idx && *cbak_func_idx == j)
maxInstrCountCbak = *valid;
else
// Step 5: Store local WCE and build bidirectional call
// relationships
if (call_graph.find(actual_func_idx) != call_graph.end())
{
if (DEBUG_GUARD)
printf(
"code section: %d not hook_func_idx: %d or "
"cbak_func_idx: %d\n",
j,
*hook_func_idx,
(cbak_func_idx ? *cbak_func_idx : -1));
// assert(false);
call_graph[actual_func_idx].local_wce = *valid;
// Build bidirectional relationships: for each callee, add
// this function as a caller
for (int callee_idx : call_graph[actual_func_idx].callees)
{
if (call_graph.find(callee_idx) != call_graph.end())
{
call_graph[callee_idx].callers.insert(
actual_func_idx);
}
}
}
// Note: We will calculate total WCE later after processing all
// functions
i = code_end;
}
}
i = next_section;
}
// execution to here means guards are installed correctly
// Step 6: Cycle detection using DFS
// Lambda function for DFS-based cycle detection
std::set<int> visited;
std::set<int> rec_stack;
std::function<bool(int)> detect_cycles_dfs = [&](int func_idx) -> bool {
if (rec_stack.find(func_idx) != rec_stack.end())
{
// Found a cycle: func_idx is already in the recursion stack
return true;
}
return std::pair<uint64_t, uint64_t>{maxInstrCountHook, maxInstrCountCbak};
if (visited.find(func_idx) != visited.end())
{
// Already visited and no cycle found from this node
return false;
}
visited.insert(func_idx);
rec_stack.insert(func_idx);
// Check all callees
if (call_graph.find(func_idx) != call_graph.end())
{
for (int callee_idx : call_graph[func_idx].callees)
{
if (detect_cycles_dfs(callee_idx))
{
return true;
}
}
}
rec_stack.erase(func_idx);
return false;
};
// Run cycle detection on all user-defined functions
for (const auto& [func_idx, func_info] : call_graph)
{
if (detect_cycles_dfs(func_idx))
{
GUARDLOG(hook::log::CALL_ILLEGAL)
<< "GuardCheck: Recursive function calls detected. "
<< "Hooks cannot contain recursive or mutually recursive "
"functions.\n";
return {};
}
}
// Step 7: Calculate total WCE for each function using bottom-up approach
// Lambda function for recursive WCE calculation with memoization
std::function<uint64_t(int)> calculate_function_wce =
[&](int func_idx) -> uint64_t {
// Check if function exists in call graph
if (call_graph.find(func_idx) == call_graph.end())
{
// This is an imported function, WCE = 0 (already accounted for)
return 0;
}
FunctionInfo& func_info = call_graph[func_idx];
// If already calculated, return cached result
if (func_info.wce_calculated)
{
return func_info.total_wce;
}
// Detect circular dependency in WCE calculation (should not happen
// after cycle detection)
if (func_info.in_calculation)
{
GUARDLOG(hook::log::CALL_ILLEGAL)
<< "GuardCheck: Internal error - circular dependency detected "
"during WCE calculation.\n";
return 0xFFFFFFFFU; // Return large value to trigger overflow error
}
func_info.in_calculation = true;
// Start with local WCE
uint64_t total = func_info.local_wce;
// Add WCE of all callees
for (int callee_idx : func_info.callees)
{
uint64_t callee_wce = calculate_function_wce(callee_idx);
// Check for overflow
if (total > 0xFFFFU || callee_wce > 0xFFFFU ||
(total + callee_wce) > 0xFFFFU)
{
func_info.in_calculation = false;
return 0xFFFFFFFFU; // Signal overflow
}
total += callee_wce;
}
func_info.total_wce = total;
func_info.wce_calculated = true;
func_info.in_calculation = false;
return total;
};
// Calculate WCE for hook and cbak functions
int64_t hook_wce_actual = 0;
int64_t cbak_wce_actual = 0;
if (hook_func_idx)
{
int actual_hook_idx = last_import_number + 1 + *hook_func_idx;
hook_wce_actual = calculate_function_wce(actual_hook_idx);
if (hook_wce_actual >= 0xFFFFU)
{
GUARDLOG(hook::log::INSTRUCTION_EXCESS)
<< "GuardCheck: hook() function exceeds maximum instruction "
"count (65535). "
<< "Total WCE including called functions: " << hook_wce_actual
<< "\n";
return {};
}
if (DEBUG_GUARD)
printf("hook() total WCE: %ld\n", hook_wce_actual);
}
if (cbak_func_idx)
{
int actual_cbak_idx = last_import_number + 1 + *cbak_func_idx;
cbak_wce_actual = calculate_function_wce(actual_cbak_idx);
if (cbak_wce_actual >= 0xFFFFU)
{
GUARDLOG(hook::log::INSTRUCTION_EXCESS)
<< "GuardCheck: cbak() function exceeds maximum instruction "
"count (65535). "
<< "Total WCE including called functions: " << cbak_wce_actual
<< "\n";
return {};
}
if (DEBUG_GUARD)
printf("cbak() total WCE: %ld\n", cbak_wce_actual);
}
// execution to here means guards are installed correctly and WCE is within
// limits
return std::pair<uint64_t, uint64_t>{hook_wce_actual, cbak_wce_actual};
}

View File

@@ -267,9 +267,6 @@ DeleteAccount::preclaim(PreclaimContext const& ctx)
if (sleAccount->isFieldPresent(sfHookNamespaces) ||
sleAccount->isFieldPresent(sfHooks))
return tecHAS_OBLIGATIONS;
if (sleAccount->isFieldPresent(sfHookAdministrator))
return tecHAS_OBLIGATIONS;
}
// When fixNFTokenRemint is enabled, we don't allow an account to be

View File

@@ -897,9 +897,7 @@ ValidNewAccountRoot::finalize(
}
if ((tt == ttPAYMENT || tt == ttIMPORT || tt == ttGENESIS_MINT ||
tt == ttREMIT ||
(tt == ttHOOK_SET &&
view.rules().enabled(featureHookAdministrator))) &&
tt == ttREMIT) &&
isTesSuccess(result))
{
std::uint32_t const startingSeq{

View File

@@ -628,21 +628,6 @@ SetHook::calculateBaseFee(ReadView const& view, STTx const& tx)
TER
SetHook::preclaim(ripple::PreclaimContext const& ctx)
{
if (ctx.tx.isFieldPresent(sfHookAdministrator))
{
auto const& administrator = ctx.tx.getAccountID(sfHookAdministrator);
auto const& sle = ctx.view.read(keylet::account(administrator));
if (!sle)
return tecNO_DST;
if (!sle->isFieldPresent(sfHookAdministrator))
return tecNO_PERMISSION;
if (sle->getAccountID(sfHookAdministrator) !=
ctx.tx.getAccountID(sfAccount))
return tecNO_PERMISSION;
}
auto const& hookSets = ctx.tx.getFieldArray(sfHooks);
for (auto const& hookSetObj : hookSets)
@@ -682,46 +667,12 @@ SetHook::preflight(PreflightContext const& ctx)
return ret;
if (ctx.rules.enabled(fixInvalidTxFlags) &&
ctx.tx.getFlags() & tfSetHookMask)
ctx.tx.getFlags() & tfUniversalMask)
{
JLOG(ctx.j.trace()) << "SetHook: Invalid flags set.";
return temINVALID_FLAG;
}
if (ctx.tx.isFlag(tfNewAccount) &&
!ctx.rules.enabled(featureHookAdministrator))
{
JLOG(ctx.j.trace()) << "SetHook: New account flag set but hook "
"administrator amendment is not enabled.";
return temDISABLED;
}
if (ctx.tx.isFieldPresent(sfDestination))
{
if (!ctx.rules.enabled(featureHookAdministrator))
{
JLOG(ctx.j.trace())
<< "HookSet: Hook administrator amendment not enabled.";
return temDISABLED;
}
if (ctx.tx.isFlag(tfNewAccount))
{
JLOG(ctx.j.trace())
<< "HookSet: Both new account flag and destination set. "
"New account flag and destination cannot be set at the same "
"time.";
return temMALFORMED;
}
if (ctx.tx.getAccountID(sfDestination) ==
ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.trace()) << "HookSet: Redundant hook administrator.";
return temREDUNDANT;
}
}
if (!ctx.tx.isFieldPresent(sfHooks))
{
JLOG(ctx.j.trace())
@@ -1232,23 +1183,6 @@ struct KeyletComparator
}
};
AccountID
randomAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey)
{
// This number must not be changed without an amendment
constexpr std::uint16_t maxAccountAttempts = 256;
for (std::uint16_t i = 0; i < maxAccountAttempts; ++i)
{
ripesha_hasher rsh;
auto const hash = sha512Half(i, view.info().parentHash, pseudoOwnerKey);
rsh(hash.data(), hash.size());
AccountID const ret{static_cast<ripesha_hasher::result_type>(rsh)};
if (!view.read(keylet::account(ret)))
return ret;
}
return beast::zero;
}
TER
SetHook::setHook()
{
@@ -1268,69 +1202,11 @@ SetHook::setHook()
.app = ctx_.app,
.rules = ctx_.view().rules()};
auto targetAccount = ctx.tx[~sfDestination].value_or(account_);
if (ctx_.tx.isFlag(tfNewAccount))
{
// create the new account
auto const newAccount = randomAccountAddress(ctx_.view(), uint256{});
if (newAccount == beast::zero)
return tecDUPLICATE;
auto sleNewAccount = std::make_shared<SLE>(keylet::account(newAccount));
sleNewAccount->setAccountID(sfAccount, newAccount);
sleNewAccount->setFieldAmount(sfBalance, STAmount{});
sleNewAccount->setFieldU32(sfOwnerCount, 1); // ltHook
std::uint32_t const seqno{
ctx_.view().rules().enabled(featureXahauGenesis)
? ctx_.view().info().parentCloseTime.time_since_epoch().count()
: ctx_.view().rules().enabled(featureDeletableAccounts)
? ctx_.view().seq()
: 1};
sleNewAccount->setFieldU32(sfSequence, seqno);
sleNewAccount->setFieldU32(sfFlags, lsfDisableMaster);
sleNewAccount->setAccountID(sfHookAdministrator, account_);
auto sleFees = view().peek(keylet::fees());
if (sleFees && view().rules().enabled(featureXahauGenesis))
{
auto actIdx = sleFees->isFieldPresent(sfAccountCount)
? sleFees->getFieldU64(sfAccountCount)
: 0;
sleNewAccount->setFieldU64(sfAccountIndex, actIdx);
sleFees->setFieldU64(sfAccountCount, actIdx + 1);
view().update(sleFees);
}
// fund AccountReserve + ObjectReserve (ltHook)
auto const requiredDrops = ctx_.view().fees().accountReserve(1);
auto sourceSle = ctx_.view().peek(keylet::account(account_));
if (!sourceSle)
return tefINTERNAL;
auto const sourceCurrentReserve = ctx_.view().fees().accountReserve(
sourceSle->getFieldU32(sfOwnerCount));
auto const sourceBalance = sourceSle->getFieldAmount(sfBalance).xrp();
if (sourceBalance < sourceCurrentReserve + requiredDrops)
return tecUNFUNDED;
sourceSle->setFieldAmount(sfBalance, sourceBalance - requiredDrops);
ctx_.view().update(sourceSle);
sleNewAccount->setFieldAmount(sfBalance, requiredDrops);
ctx_.view().insert(sleNewAccount);
targetAccount = newAccount;
}
const int blobMax = hook::maxHookWasmSize();
auto const accountKeylet = keylet::account(account_);
auto const hookKeylet = keylet::hook(account_);
auto const hookKeylet = keylet::hook(targetAccount);
auto accountSLE = view().peek(keylet::account(targetAccount));
auto accountSLE = view().peek(accountKeylet);
ripple::STArray newHooks{sfHooks, 8};
auto newHookSLE = std::make_shared<SLE>(hookKeylet);

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 91;
static constexpr std::size_t numFeatures = 90;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -378,7 +378,6 @@ extern uint256 const fixInvalidTxFlags;
extern uint256 const featureExtendedHookState;
extern uint256 const fixCronStacking;
extern uint256 const fixHookAPI20251128;
extern uint256 const featureHookAdministrator;
} // namespace ripple
#endif

View File

@@ -563,7 +563,6 @@ extern SF_ACCOUNT const sfEmitCallback;
extern SF_ACCOUNT const sfHookAccount;
extern SF_ACCOUNT const sfNFTokenMinter;
extern SF_ACCOUNT const sfInform;
extern SF_ACCOUNT const sfHookAdministrator;
// path set
extern SField const sfPaths;

View File

@@ -184,11 +184,6 @@ constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal);
// NFTokenAcceptOffer flags:
constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal;
enum SetHookFlags : uint32_t {
tfNewAccount = 0x00000001,
};
constexpr std::uint32_t const tfSetHookMask = ~(tfUniversal | tfNewAccount);
// URIToken mask
constexpr std::uint32_t const tfURITokenMintMask = ~(tfUniversal | tfBurnable);
constexpr std::uint32_t const tfURITokenNonMintMask = ~tfUniversal;

View File

@@ -484,7 +484,6 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De
REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FEATURE(HookAdministrator, Supported::yes, VoteBehavior::DefaultNo);
// The following amendments are obsolete, but must remain supported
// because they could potentially get enabled.

View File

@@ -70,7 +70,6 @@ LedgerFormats::LedgerFormats()
{sfTouchCount, soeOPTIONAL},
{sfHookStateScale, soeOPTIONAL},
{sfCron, soeOPTIONAL},
{sfHookAdministrator, soeOPTIONAL},
},
commonFields);

View File

@@ -315,7 +315,6 @@ CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT,
// account (uncommon)
CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16);
CONSTRUCT_TYPED_SFIELD(sfHookAdministrator, "HookAdministrator", ACCOUNT, 98);
CONSTRUCT_TYPED_SFIELD(sfInform, "Inform", ACCOUNT, 99);
// vector of 256-bit

View File

@@ -324,7 +324,6 @@ TxFormats::TxFormats()
{
{sfHooks, soeREQUIRED},
{sfTicketSequence, soeOPTIONAL},
{sfDestination, soeOPTIONAL},
},
commonFields);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -58,8 +58,21 @@ cat $INPUT_FILE | tr '\n' '\f' |
then
echo '#include "api.h"' > "$WASM_DIR/test-$COUNTER-gen.c"
tr '\f' '\n' <<< $line >> "$WASM_DIR/test-$COUNTER-gen.c"
DECLARED="`tr '\f' '\n' <<< $line | grep -E '(extern|define) ' | grep -Eo '[a-z\-\_]+ *\(' | grep -v 'sizeof' | sed -E 's/[^a-z\-\_]//g' | sort | uniq`"
USED="`tr '\f' '\n' <<< $line | grep -vE '(extern|define) ' | grep -Eo '[a-z\-\_]+\(' | grep -v 'sizeof' | sed -E 's/[^a-z\-\_]//g' | grep -vE '^(hook|cbak)' | sort | uniq`"
DECLARED="`tr '\f' '\n' <<< $line \
| grep -E '(extern|static|define) ' \
| grep -Eo '[a-z\-\_]+ *\(' \
| grep -v 'sizeof' \
| sed -E 's/[^a-z\-\_]//g' \
| grep -vE '^__attribute__$' \
| sort | uniq`"
USED="`tr '\f' '\n' <<< $line \
| grep -vE '(extern|static|define) ' \
| grep -Eo '[a-z\-\_]+\(' \
| grep -v 'sizeof' \
| sed -E 's/[^a-z\-\_]//g' \
| grep -vE '^(__attribute__|hook|cbak)$' \
| sort | uniq`"
ONCE="`echo $DECLARED $USED | tr ' ' '\n' | sort | uniq -c | grep '1 ' | sed -E 's/^ *1 //g'`"
FILTER="`echo $DECLARED | tr ' ' '|' | sed -E 's/\|$//g'`"
UNDECL="`echo $ONCE | grep -v -E $FILTER 2>/dev/null || echo ''`"
@@ -69,7 +82,7 @@ cat $INPUT_FILE | tr '\n' '\f' |
echo "$line"
exit 1
fi
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined <<< "`tr '\f' '\n' <<< $line`" |
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined,--export=hook,--export=cbak <<< "`tr '\f' '\n' <<< $line`" |
hook-cleaner - - 2>/dev/null |
xxd -p -u -c 10 |
sed -E 's/../0x&U,/g' | sed -E 's/^/ /g' >> $OUTPUT_FILE