From 32a7e63a69bf5566e711683bcb8b2c299284639f Mon Sep 17 00:00:00 2001 From: OpadijoIdris Date: Thu, 16 Jul 2026 18:00:03 +0100 Subject: [PATCH] fix: sanitize taxonomy JSON output --- .../core-integration/fetch-taxonomy/index.js | 106 ++++++++++++- .../fetch-taxonomy/index.test.js | 14 ++ .../fetch-taxonomy/taxonomy.json | 149 ++++++++++++++++++ 3 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 examples/core-integration/fetch-taxonomy/index.test.js create mode 100644 examples/core-integration/fetch-taxonomy/taxonomy.json diff --git a/examples/core-integration/fetch-taxonomy/index.js b/examples/core-integration/fetch-taxonomy/index.js index a6b3dbdf..95cf948b 100644 --- a/examples/core-integration/fetch-taxonomy/index.js +++ b/examples/core-integration/fetch-taxonomy/index.js @@ -1 +1,105 @@ -// Dummy implementation for Fetch Taxonomy +#!/usr/bin/env node + +const fs = require('fs').promises; +const path = require('path'); +const toml = require('@iarna/toml'); + +function sanitizeText(value) { + if (typeof value !== 'string') { + return value; + } + + return value + .replace(/\r\n?/g, '\n') + .replace(/[ \t]+$/gm, '') + .replace(/[ \t]+(?=\n)/g, '') + .replace(/\n{3,}/g, '\n\n') + .trim(); +} + +function normalizeErrorCode(value) { + if (typeof value === 'number') { + return Number.isSafeInteger(value) ? value : String(value); + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (/^-?\d+$/.test(trimmed)) { + const parsed = BigInt(trimmed); + return parsed > BigInt(Number.MAX_SAFE_INTEGER) || parsed < BigInt(-Number.MAX_SAFE_INTEGER) + ? trimmed + : Number(parsed); + } + + return trimmed; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + return value; +} + +function sanitizeAndNormalize(value, key = '') { + if (Array.isArray(value)) { + return value.map((item) => sanitizeAndNormalize(item, key)); + } + + if (value && typeof value === 'object') { + return Object.entries(value).reduce((acc, [entryKey, entryValue]) => { + const nextKey = key ? `${key}.${entryKey}` : entryKey; + let normalizedValue = entryValue; + + if (entryKey === 'code' || entryKey === 'error_code') { + normalizedValue = normalizeErrorCode(entryValue); + } else if (typeof entryValue === 'string' && /description|summary|explanation|name|severity|category|difficulty|likelihood|source|id/i.test(entryKey)) { + normalizedValue = sanitizeText(entryValue); + } else if (entryValue && typeof entryValue === 'object') { + normalizedValue = sanitizeAndNormalize(entryValue, nextKey); + } + + acc[entryKey] = normalizedValue; + return acc; + }, {}); + } + + if (typeof value === 'string' && /description|summary|explanation|name|severity|category|difficulty|likelihood|source|id/i.test(key)) { + return sanitizeText(value); + } + + return value; +} + +async function buildTaxonomyJson({ inputPath, outputPath }) { + const input = await fs.readFile(inputPath, 'utf8'); + const parsed = toml.parse(input); + const sanitized = sanitizeAndNormalize(parsed); + const serialized = JSON.stringify(sanitized, null, 2) + '\n'; + await fs.writeFile(outputPath, serialized, 'utf8'); + return serialized; +} + +async function main() { + const workspaceRoot = path.resolve(__dirname, '..', '..', '..'); + const inputPath = path.join(workspaceRoot, 'crates', 'core', 'src', 'taxonomy', 'data', 'contract.toml'); + const outputPath = path.join(__dirname, 'taxonomy.json'); + + const serialized = await buildTaxonomyJson({ inputPath, outputPath }); + console.log(`Wrote sanitized taxonomy JSON to ${path.relative(workspaceRoot, outputPath)}`); + return serialized; +} + +if (require.main === module) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} + +module.exports = { + sanitizeText, + normalizeErrorCode, + sanitizeAndNormalize, + buildTaxonomyJson, +}; diff --git a/examples/core-integration/fetch-taxonomy/index.test.js b/examples/core-integration/fetch-taxonomy/index.test.js new file mode 100644 index 00000000..3c9f8ac3 --- /dev/null +++ b/examples/core-integration/fetch-taxonomy/index.test.js @@ -0,0 +1,14 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { sanitizeText, normalizeErrorCode } = require('./index'); + +test('sanitizeText trims and normalizes pesky whitespace', () => { + const value = ' Contract error\r\nwith trailing spaces \r\n'; + assert.equal(sanitizeText(value), 'Contract error\nwith trailing spaces'); +}); + +test('normalizeErrorCode preserves unsafe integers without losing precision', () => { + assert.equal(normalizeErrorCode(42), 42); + assert.equal(normalizeErrorCode('9007199254740993'), '9007199254740993'); + assert.equal(normalizeErrorCode(BigInt('9007199254740993')), '9007199254740993'); +}); diff --git a/examples/core-integration/fetch-taxonomy/taxonomy.json b/examples/core-integration/fetch-taxonomy/taxonomy.json new file mode 100644 index 00000000..9fc3b5a8 --- /dev/null +++ b/examples/core-integration/fetch-taxonomy/taxonomy.json @@ -0,0 +1,149 @@ +{ + "category": { + "name": "Contract", + "description": "Errors raised by contract code itself (not the host). These use numeric codes defined in the contract's error enum.", + "source_module": "soroban-env-host/src/host.rs" + }, + "errors": [ + { + "id": "host.contract.error_general", + "category": "contract", + "code": 0, + "name": "ContractError", + "severity": "Error", + "since_protocol": 20, + "summary": "Contract error: the contract's own logic rejected this call — run with --resolve to map the code to its name.", + "detailed_explanation": "Unlike host errors, contract errors are defined by the contract author. Each contract can define an error enum with numeric codes and descriptive names. When a contract panics or returns an error value, the host wraps it as a ContractError with the numeric code.\n\nTo understand a contract error, you need the contract's specification (contractspecv0 metadata) which maps the numeric code to a human-readable name and optional documentation.", + "common_causes": [ + { + "description": "Business logic assertion failure in the contract (e.g., insufficient balance, invalid state)", + "likelihood": "high" + }, + { + "description": "Input validation failure — the contract rejected the provided arguments", + "likelihood": "high" + }, + { + "description": "Access control check failed — the caller is not authorized for this operation", + "likelihood": "medium" + } + ], + "suggested_fixes": [ + { + "description": "Use grat's contract error resolver to map the numeric code to the contract's error enum name", + "difficulty": "easy", + "requires_upgrade": false + }, + { + "description": "Review the contract source code for the error enum definition to understand the specific failure", + "difficulty": "medium", + "requires_upgrade": false, + "related_errors": [ + "host.auth.not_authorized" + ], + "source_file": "soroban-env-host/src/host.rs" + } + ] + }, + { + "id": "host.contract.internal_error", + "category": "contract", + "code": 1, + "name": "InternalError", + "severity": "Error", + "since_protocol": 20, + "summary": "An internal protocol implementation error occurred (e.g. invalid ledger state).", + "detailed_explanation": "The contract encountered an internal error during execution, indicating a protocol implementation issue or invalid ledger state.", + "common_causes": [ + { + "description": "Internal validation or state consistency checks failed in the contract", + "likelihood": "high" + } + ], + "suggested_fixes": [ + { + "description": "Check the diagnostic logs to see if there are internal contract assertion failures", + "difficulty": "medium", + "requires_upgrade": false, + "related_errors": [], + "source_file": "soroban-env-host/src/host.rs" + } + ] + }, + { + "id": "host.contract.operation_not_supported", + "category": "contract", + "code": 2, + "name": "OperationNotSupportedError", + "severity": "Error", + "since_protocol": 20, + "summary": "The operation is not supported (e.g. calling clawback on an asset without clawback enabled).", + "detailed_explanation": "The contract attempted an operation that is unsupported by the contract configuration or type (e.g., performing a clawback on an asset when clawback is disabled).", + "common_causes": [ + { + "description": "Invoking clawback on a non-clawbackable asset", + "likelihood": "high" + } + ], + "suggested_fixes": [ + { + "description": "Check the asset flags and ensure the operation is allowed for the target asset", + "difficulty": "easy", + "requires_upgrade": false, + "related_errors": [], + "source_file": "soroban-env-host/src/host.rs" + } + ] + }, + { + "id": "host.contract.already_initialized", + "category": "contract", + "code": 3, + "name": "AlreadyInitializedError", + "severity": "Error", + "since_protocol": 20, + "summary": "The contract instance has already been initialized and cannot be re-initialized.", + "detailed_explanation": "The contract instance was initialized twice. Soroban contracts typically only allow initialization once.", + "common_causes": [ + { + "description": "Calling the initialize function on an already initialized contract instance", + "likelihood": "high" + } + ], + "suggested_fixes": [ + { + "description": "Ensure that initialization is only called once per contract deployment", + "difficulty": "easy", + "requires_upgrade": false, + "related_errors": [], + "source_file": "soroban-env-host/src/host.rs" + } + ] + }, + { + "id": "host.contract.account_missing", + "category": "contract", + "code": 6, + "name": "AccountMissingError", + "severity": "Error", + "since_protocol": 20, + "summary": "An account involved in the transaction does not exist on the network.", + "detailed_explanation": "The operation required a specific account to exist on the network, but the account could not be found.", + "common_causes": [ + { + "description": "Providing an invalid account address or an account that has not been created/funded", + "likelihood": "high" + } + ], + "suggested_fixes": [ + { + "description": "Verify that the addresses provided exist and are active on the target network", + "difficulty": "easy", + "requires_upgrade": false, + "related_errors": [], + "source_file": "soroban-env-host/src/host.rs" + } + ] + } + ] +}