This is an automated email from the ASF dual-hosted git repository.
CoverRyan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil-vscode.git
The following commit(s) were added to refs/heads/main by this push:
new 2877674e add feature to prevent invalid tunables
2877674e is described below
commit 2877674efa8debc92ffd2d6595e2f106b685558e
Author: Adedoyin Ogunjobi <[email protected]>
AuthorDate: Wed Jun 24 14:43:49 2026 -0400
add feature to prevent invalid tunables
changes to scripts.js
dont let users save invalud key or value
move tunables.json and add feature
Document allowed tunables and error handling
Added section on allowed tunables and their validation process.
Exclude tunables.json from RAT and prettier
documentation + more tunables + verifiy values
fix error not showing up. only the first error will show
make invalids red
proper fix for rendering error
Make error show for each invalid tunable
add fetch retry logic
Update sbt-scalafmt to 2.6.1
Update scalafmt-core to 3.11.1
---
constants/tunables.json | 76 ++++++++++++++++++++
doc/Wiki.md | 11 +++
project/Rat.scala | 1 +
src/adapter/activateDaffodilDebug.ts | 62 +++++++++++++++-
src/adapter/extension.ts | 13 ++++
src/launchWizard/launchWizard.ts | 24 +++++++
src/launchWizard/script.js | 134 ++++++++++++++++++++++++++++++++---
7 files changed, 309 insertions(+), 12 deletions(-)
diff --git a/constants/tunables.json b/constants/tunables.json
new file mode 100644
index 00000000..2b85a4d7
--- /dev/null
+++ b/constants/tunables.json
@@ -0,0 +1,76 @@
+{
+ "allowExpressionResultCoercion": "boolean",
+ "allowExternalPathExpressions": "boolean",
+ "allowSignedIntegerLength1Bit": "boolean",
+
+ "blobChunkSizeInBytes": "number",
+
+ "defaultEmptyElementParsePolicy": "string",
+
+ "escalateWarningsToErrors": "boolean",
+
+ "generatedNamespacePrefixStem": "string",
+
+ "initialElementOccurrencesHint": "number",
+ "initialRegexMatchLimitInCharacters": "number",
+
+ "infosetWalkerSkipMin": "number",
+ "infosetWalkerSkipMax": "number",
+
+ "invalidRestrictionPolicy": {
+ "type": "enum",
+ "values": ["error", "ignore", "validate"]
+ },
+
+ "maxBinaryDecimalVirtualPoint": "number",
+ "maxByteArrayOutputStreamBufferSizeInBytes": "number",
+ "maxDataDumpSizeInBytes": "number",
+ "maxHexBinaryLengthInBytes": "number",
+
+ "maxLengthForVariableLengthDelimiterDisplay": "number",
+ "maxLookaheadFunctionBits": "number",
+
+ "maxOccursBounds": "number",
+
+ "maxSkipLengthInBytes": "number",
+
+ "maxValidYear": "number",
+
+ "maximumRegexMatchLengthInCharacters": "number",
+ "maximumSimpleElementSizeInCharacters": "number",
+
+ "minBinaryDecimalVirtualPoint": "number",
+ "minValidYear": "number",
+
+ "outputStreamChunkSizeInBytes": "number",
+
+ "parseUnparsePolicy": "string",
+
+ "releaseUnneededInfoset": "boolean",
+
+ "requireBitOrderProperty": "boolean",
+ "requireEmptyElementParsePolicyProperty": "boolean",
+
+ "requireEncodingErrorPolicyProperty": "boolean",
+ "requireFloatingProperty": "boolean",
+ "requireTextBidiProperty": "boolean",
+ "requireTextStandardBaseProperty": "boolean",
+
+ "saxUnparseEventBatchSize": "number",
+
+ "suppressSchemaDefinitionWarnings": "string",
+
+ "tempFilePath": "string",
+
+ "unqualifiedPathStepPolicy": {
+ "type": "enum",
+ "values": [
+ "noNamespace",
+ "defaultNamespace",
+ "preferDefaultNamespace"
+ ]
+ },
+
+ "unparseSuspensionWaitOld": "number",
+ "unparseSuspensionWaitYoung": "number"
+}
diff --git a/doc/Wiki.md b/doc/Wiki.md
index 2d56f7c3..d4963dee 100644
--- a/doc/Wiki.md
+++ b/doc/Wiki.md
@@ -372,12 +372,23 @@ Tunables are configured using **Key** and **Value**
fields in the launch configu
Use the **“+ Add Tunable”** button to add additional entries. Each row can be
removed using the **“X”** button.
+Tunables are case sensitive, and only allowed tunables will be allowed to be
saved in the wizard.
+
Tunables control runtime behavior and performance characteristics. Most
tunables have default values, so you only need to specify a value if you want
to override them.
<img width="790" height="176" alt="tunables_gui"
src="https://github.com/user-attachments/assets/f78d5471-bfff-4cfb-b6cf-62c787a8e31b"
/>
<img width="360" height="177" alt="tunables_launchjson"
src="https://github.com/user-attachments/assets/0d70044d-d04c-4f47-958c-175c6e67f3f9"
/>
+### List of allowed Tunables
+
+In order to prevent unexpected tunables from being ran, there is a list of
allowed tunables at `constants/tunables.json` which contains all valid
tunables. Tunables are case sensitive and the values type must match what is
expected. For example boolean values will only allow true or false. If a
invalid tunable is saved in the `launch.json` , when you attempt to run
daffodil you will encounter a error saying `Cancel` or `Ignore invalid
tunable`. Cancel will send you back where you fix the [...]
+
+The validation for tunable values is reasonably loose to allow for possibly
unknown tunables to be applied. For example suppressSchemaDefinitionWarnings
documentation didn't explicitly list all the possible values, but
unqualifiedPathStepPolicy explicitly stated the allowed values so only those
values are allowing.
+
+<img width="914" height="718" alt="image"
src="https://github.com/user-attachments/assets/22b24b29-de7e-4ec9-b012-85fe8f1bb54c"
/>
+
+
### Reference
- https://daffodil.apache.org/tunables/
diff --git a/project/Rat.scala b/project/Rat.scala
index e8b214e1..3fc9056c 100644
--- a/project/Rat.scala
+++ b/project/Rat.scala
@@ -52,6 +52,7 @@ object Rat {
file("build/package/NOLICENSE"),
file("build/package/NONOTICE"),
file("src/tests/data/test.txt"),
+ file("constants/tunables.json"),
file("debugger/src/test/data/emptyData.xml"),
file("debugger/src/test/data/emptyInfoset.xml"),
file("debugger/src/test/data/notInfoset.xml"),
diff --git a/src/adapter/activateDaffodilDebug.ts
b/src/adapter/activateDaffodilDebug.ts
index 3f523def..dafdc92c 100644
--- a/src/adapter/activateDaffodilDebug.ts
+++ b/src/adapter/activateDaffodilDebug.ts
@@ -18,6 +18,7 @@ import * as rootCompletion from '../rootCompletion'
import { tmpdir } from 'os'
import JSZip from 'jszip'
import { rm } from 'node:fs/promises'
+import { getTunables } from './extension'
import {
CancellationToken,
@@ -766,16 +767,15 @@ class DaffodilConfigurationProvider
constructor(context: vscode.ExtensionContext) {
this.context = context
}
-
/**
* Massage a debug configuration just before a debug session is being
launched,
* e.g. add all missing attributes to the debug configuration.
*/
- resolveDebugConfiguration(
+ async resolveDebugConfiguration(
folder: WorkspaceFolder | undefined,
config: DebugConfiguration,
token?: CancellationToken
- ): ProviderResult<DebugConfiguration> {
+ ): Promise<DebugConfiguration | undefined> {
// if launch.json is missing or empty
if (!config.type && !config.request && !config.name) {
config = getConfig({ name: 'Launch', request: 'launch', type: 'dfdl' })
@@ -791,6 +791,62 @@ class DaffodilConfigurationProvider
data: config.data || '${command:AskForDataName}',
}
+ const validTunables = getTunables(this.context)
+ const currentTunables = config.tunables ?? {}
+
+ const invalidTunables = Object.keys(currentTunables).filter(
+ (name) => !(name in validTunables)
+ )
+
+ const invalidValues = Object.entries(currentTunables)
+ .filter(([name, value]) => {
+ if (!(name in validTunables)) {
+ return false
+ }
+
+ const type = validTunables[name]
+
+ switch (type) {
+ case 'boolean':
+ return value !== 'true' && value !== 'false'
+
+ case 'number':
+ return isNaN(Number(value))
+
+ case 'string':
+ return typeof value !== 'string'
+
+ default:
+ return false
+ }
+ })
+ .map(([name]) => name)
+
+ const invalid = [...invalidTunables, ...invalidValues]
+
+ if (invalid.length > 0) {
+ const messages = [
+ ...invalidTunables.map((name) => `${name} (invalid tunable)`),
+ ...invalidValues.map((name) => `${name} (invalid value)`),
+ ]
+
+ const choice = await vscode.window.showWarningMessage(
+ `Invalid tunables found:\n\n${messages.join('\n')}`,
+ { modal: true },
+ 'Ignore Invalid Tunables'
+ )
+
+ if (choice !== 'Ignore Invalid Tunables') {
+ return undefined
+ }
+
+ config.tunables = { ...config.tunables }
+
+ for (const invalidTunable of invalid) {
+ delete config.tunables[invalidTunable]
+ }
+ }
+
let dataFolder = config.data
if (
diff --git a/src/adapter/extension.ts b/src/adapter/extension.ts
index 13418217..fd8aa172 100644
--- a/src/adapter/extension.ts
+++ b/src/adapter/extension.ts
@@ -19,6 +19,8 @@
import * as vscode from 'vscode'
import * as position from '../position'
+import * as fs from 'fs'
+import * as path from 'path'
import { ProviderResult } from 'vscode'
import { DaffodilDebugSession } from './daffodilDebug'
import {
@@ -45,6 +47,17 @@ export function deactivate() {
position.deactivate()
}
+export function getTunables(
+ context: vscode.ExtensionContext
+): Record<string, string> {
+ const file = path.join(context.extensionPath, 'constants', 'tunables.json')
+
+ const data = fs.readFileSync(file, 'utf8')
+ const json = JSON.parse(data)
+
+ return json
+}
+
export class InlineDebugAdapterFactory
implements vscode.DebugAdapterDescriptorFactory
{
diff --git a/src/launchWizard/launchWizard.ts b/src/launchWizard/launchWizard.ts
index c130a8fe..63ff2a19 100644
--- a/src/launchWizard/launchWizard.ts
+++ b/src/launchWizard/launchWizard.ts
@@ -22,6 +22,7 @@ import { DFDLDebugger } from '../classes/dfdlDebugger'
import { VSCodeLaunchConfigArgs } from '../classes/vscode-launch'
import { DataEditorConfig } from '../classes/dataEditor'
import { parse as jsoncParse } from 'jsonc-parser'
+import * as path from 'path'
let launchWizard: LaunchWizard | undefined
@@ -279,6 +280,20 @@ async function createWizard(ctx: vscode.ExtensionContext) {
let panel = launchWiz.getPanel()
panel.webview.html = launchWiz.getWebViewContent()
+ //Read in list of valid
+
+ const tunablesPath = path.join(
+ ctx.extensionPath,
+ 'constants',
+ 'tunables.json'
+ )
+
+ const tunables = JSON.parse(fs.readFileSync(tunablesPath, 'utf8'))
+ panel.webview.postMessage({
+ command: 'loadTunables',
+ tunables: tunables,
+ })
+
panel.webview.onDidReceiveMessage(
async (message) => {
switch (message.command) {
@@ -777,11 +792,20 @@ class LaunchWizard {
</button>
</div>
+ <div id="tunableErrorContainer"
+ style="color:red;font-size:12px;">
+ </div>
+
<div id="VariablesDiv" class="setting-div" style="margin-top: 15px;">
<p>Variables:</p>
<p class="setting-description">Key/value configuration options</p>
<table style="width: 100%; border-collapse: collapse; margin-top:
5px;">
+ <style>
+ td {
+ vertical-align: top;
+ }
+ </style>
<thead>
<tr>
<th style="text-align: left;">Key</th>
diff --git a/src/launchWizard/script.js b/src/launchWizard/script.js
index 63811c55..366ae663 100644
--- a/src/launchWizard/script.js
+++ b/src/launchWizard/script.js
@@ -348,6 +348,19 @@ function save() {
)
}
+ // Blocks save if tunable isnt valid. This means we have to always make sure
the tunable list is valid.
+ // should we just warn the user that it will case errors and let them submit
+ validateTunablesTable()
+
+ const hasErrors = [
+ ...document.querySelectorAll('#tunablesTableBody .tunable-error'),
+ ].some((el) => el.textContent.trim())
+
+ if (hasErrors) {
+ console.warn('Cannot save: invalid tunables present')
+ return
+ }
+
vscode.postMessage({
command: 'saveConfig',
data: JSON.stringify(obj, null, 4),
@@ -361,14 +374,22 @@ function addTunableRow() {
const row = document.createElement('tr')
row.innerHTML = `
- <td><input class="file-input" /></td>
- <td><input class="file-input" /></td>
- <td><button onclick="this.closest('tr').remove()">X</button></td>
+ <td style="position: relative; vertical-align: top;">
+ <input class="file-input" />
+ <div class="tunable-error"
+ style="color:red;font-size:12px;position:absolute;top:100%;left:0;">
+ </div>
+ </td>
+ <td style="vertical-align: top;">
+ <input class="file-input" />
+ </td>
+ <td style="vertical-align: top;">
+ <button onclick="removeTunableRow(this)">X</button>
+ </td>
`
tableBody.appendChild(row)
}
-
function getTunablesFromTable() {
const rows = document.querySelectorAll('#tunablesTableBody tr')
const tunables = {}
@@ -394,6 +415,12 @@ function escapeHtml(str) {
.replace(/'/g, ''')
}
+function removeTunableRow(btn) {
+ const row = btn.closest('tr')
+ row.remove()
+ validateTunablesTable()
+}
+
// function to pull tunables from config and render them in the tunables
table, if there are any
function renderTunables(tunables = {}) {
const tableBody = document.getElementById('tunablesTableBody')
@@ -402,11 +429,20 @@ function renderTunables(tunables = {}) {
Object.entries(tunables).forEach(([key, value]) => {
const row = document.createElement('tr')
- row.innerHTML = `
- <td><input class="file-input" value="${escapeHtml(key)}" /></td>
- <td><input class="file-input" value="${escapeHtml(value)}" /></td>
- <td><button onclick="this.closest('tr').remove()">X</button></td>
- `
+ row.innerHTML = row.innerHTML = `
+ <td>
+ <input class="file-input" value="${escapeHtml(key)}" />
+ <div class="tunable-error" style="color:red;font-size:12px;"></div>
+ </td>
+
+ <td>
+ <input class="file-input" value="${escapeHtml(value)}" />
+ </td>
+
+ <td>
+ <button onclick="this.closest('tr').remove();
validateTunablesTable();">X</button>
+ </td>
+ `
tableBody.appendChild(row)
})
@@ -463,7 +499,77 @@ function renderVariables(variables = {}) {
tableBody.appendChild(row)
})
}
+let VALID_TUNABLES = {}
+
+// Validates tunables. validates against list of tunables and the value
expected
+function validateTunablesTable() {
+ const rows = document.querySelectorAll('#tunablesTableBody tr')
+ const errorContainer = document.getElementById('tunableErrorContainer')
+
+ errorContainer.innerText = ''
+ let hasError = false
+ const errors = []
+
+ for (const row of rows) {
+ const keyInput = row.children[0].querySelector('input')
+ const valueInput = row.children[1].querySelector('input')
+
+ if (!keyInput || !valueInput) continue
+
+ const key = keyInput.value.trim()
+ const value = valueInput.value.trim()
+
+ let error = ''
+
+ // Reset styles
+ keyInput.style.color = 'white'
+ valueInput.style.color = 'white'
+
+ // Validate key
+ if (key && !VALID_TUNABLES[key]) {
+ const match = Object.keys(VALID_TUNABLES).find(
+ (t) => t.toLowerCase() === key.toLowerCase()
+ )
+
+ keyInput.style.color = 'red'
+ valueInput.style.color = 'red'
+
+ error = match
+ ? `Invalid tunable "${key}". Did you mean "${match}"?`
+ : `Invalid tunable "${key}"`
+ }
+ // Validate value
+ else if (key && value) {
+ const expectedType = VALID_TUNABLES[key]
+
+ if (expectedType === 'boolean') {
+ if (value !== 'true' && value !== 'false') {
+ valueInput.style.color = 'red'
+ error = 'Value must be boolean (true/false)'
+ }
+ } else if (expectedType === 'number') {
+ if (isNaN(Number(value))) {
+ valueInput.style.color = 'red'
+ error = 'Value must be a number'
+ }
+ } else if (expectedType?.type === 'enum') {
+ if (!expectedType.values.includes(value)) {
+ valueInput.style.color = 'red'
+ error = `Value must be one of: ${expectedType.values.join(', ')}`
+ }
+ }
+ }
+
+ if (error) {
+ hasError = true
+ errors.push(error)
+ }
+ }
+
+ errorContainer.innerText = errors.join('\n')
+ return !hasError
+}
// Function to copy selected config
function copyConfig() {
const configValues = getConfigValues()
@@ -586,6 +692,12 @@ async function updateConfigValues(config) {
renderTunables(config.tunables || {})
renderVariables(config.variables || {})
+ document
+ .getElementById('tunablesTableBody')
+ ?.addEventListener('blur', validateTunablesTable, true) // remove true and
change blur to input if we want this to validate on each key
+
+ // catches any invalid tunables on load.
+ validateTunablesTable()
updateInfosetOutputType()
updateTDMLAction()
@@ -615,6 +727,10 @@ async function updateDaffodilDebugClasspath(message) {
const message = event.data
switch (message.command) {
+ case 'loadTunables':
+ VALID_TUNABLES = message.tunables
+ // validateTunablesTable()
+ break
case 'updateConfValues':
await updateConfigValues(message.configValues)
break