134 lines
6.5 KiB
JavaScript
134 lines
6.5 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { dirname, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
|
const runtimeFile = 'src-0FGg1seX.js'
|
|
const runtimeSha256 = 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178'
|
|
const localIconsPath = resolve(rootDir, 'src/renderer/src/workbuddy/conversation/QuestionSourceIcons.tsx')
|
|
const questionPath = resolve(rootDir, 'src/renderer/src/workbuddy/conversation/QuestionFloating.tsx')
|
|
|
|
const iconSpecs = [
|
|
{ sourceName: 'HelpCircleIcon', localName: 'QuestionHelpCircleIcon', start: 911540, end: 912018 },
|
|
{ sourceName: 'ChevronLeftIcon', localName: 'QuestionChevronLeftIcon', start: 912019, end: 912255 },
|
|
{ sourceName: 'ChevronRightIcon', localName: 'QuestionChevronRightIcon', start: 912256, end: 912492 },
|
|
{ sourceName: 'CloseIcon$2', localName: 'QuestionCloseIcon', start: 245582, end: 246061 },
|
|
{ sourceName: 'RightArrowSubmitIcon', localName: 'QuestionRightArrowSubmitIcon', start: 912493, end: 912815 },
|
|
{ sourceName: 'SendUpIcon', localName: 'QuestionSendUpIcon', start: 912816, end: 913845 },
|
|
{ sourceName: 'CheckboxIcon', localName: 'QuestionCheckboxIcon', start: 913846, end: 914482 },
|
|
{ sourceName: 'EditorIcon', localName: 'QuestionEditorIcon', start: 256368, end: 257356 }
|
|
]
|
|
|
|
function sha256(bytes) {
|
|
return createHash('sha256').update(bytes).digest('hex')
|
|
}
|
|
|
|
function findDefinition(source, name, from = 0) {
|
|
const start = source.indexOf(`${name}=`, from)
|
|
if (start < 0 || /[\w$]/.test(source[start - 1] || '')) throw new Error(`REA runtime definition not found: ${name}`)
|
|
|
|
const singleQuote = String.fromCharCode(39)
|
|
const doubleQuote = String.fromCharCode(34)
|
|
const backtick = String.fromCharCode(96)
|
|
const backslash = String.fromCharCode(92)
|
|
let round = 0
|
|
let curly = 0
|
|
let square = 0
|
|
let quote = ''
|
|
let escaped = false
|
|
|
|
for (let offset = start + name.length + 1; offset < source.length; offset += 1) {
|
|
const character = source[offset]
|
|
if (quote) {
|
|
if (escaped) escaped = false
|
|
else if (character === backslash) escaped = true
|
|
else if (character === quote) quote = ''
|
|
continue
|
|
}
|
|
if (character === singleQuote || character === doubleQuote || character === backtick) {
|
|
quote = character
|
|
continue
|
|
}
|
|
if (round === 0 && curly === 0 && square === 0 && (character === ',' || character === '}')) {
|
|
return { start, end: offset, text: source.slice(start, offset) }
|
|
}
|
|
if (character === '(') round += 1
|
|
else if (character === ')') round -= 1
|
|
else if (character === '{') curly += 1
|
|
else if (character === '}') curly -= 1
|
|
else if (character === '[') square += 1
|
|
else if (character === ']') square -= 1
|
|
if (round < 0 || curly < 0 || square < 0) throw new Error(`Unbalanced REA runtime definition: ${name}`)
|
|
}
|
|
throw new Error(`REA runtime definition has no boundary: ${name}`)
|
|
}
|
|
|
|
function localFunctionBody(source, name) {
|
|
const start = source.indexOf(`export function ${name}`)
|
|
if (start < 0) throw new Error(`Local source icon is missing: ${name}`)
|
|
const end = source.indexOf('\n}\n', start)
|
|
if (end < 0) throw new Error(`Local source icon is unterminated: ${name}`)
|
|
return source.slice(start, end + 2)
|
|
}
|
|
|
|
function sourceGeometry(definition) {
|
|
const values = []
|
|
const geometryProperty = /(?:d|points|x|y|cx|cy|r|rx|ry|x1|y1|x2|y2):`([^`]+)`/g
|
|
for (const match of definition.matchAll(geometryProperty)) values.push(match[1])
|
|
for (const match of definition.matchAll(/`(var\(--cb-question-floating-[^)]+\))`/g)) values.push(match[1])
|
|
return [...new Set(values)]
|
|
}
|
|
|
|
const runtimeExport = JSON.parse(await readFile(resolve(rootDir, '.tmp/rea-runtime-scripts.json'), 'utf8'))
|
|
if (runtimeExport.evidence_id !== runtimeEvidenceId) {
|
|
throw new Error(`REA runtime Evidence ID changed: expected ${runtimeEvidenceId}, found ${runtimeExport.evidence_id ?? 'none'}`)
|
|
}
|
|
const runtimeItem = runtimeExport.items.find(item => item.file_path?.replaceAll('\\', '/').endsWith(`/assets/${runtimeFile}`))
|
|
const runtimeSource = runtimeItem?.source?.artifact?.text
|
|
if (!runtimeSource) throw new Error(`REA runtime script is missing: ${runtimeFile}`)
|
|
const actualRuntimeSha = sha256(Buffer.from(runtimeSource, 'utf8'))
|
|
if (runtimeItem.source.artifact.sha256 !== runtimeSha256 || actualRuntimeSha !== runtimeSha256) {
|
|
throw new Error(`REA runtime script digest mismatch: expected ${runtimeSha256}, export ${runtimeItem.source.artifact.sha256 ?? 'none'}, actual ${actualRuntimeSha}`)
|
|
}
|
|
|
|
const localIcons = await readFile(localIconsPath, 'utf8')
|
|
const recovered = iconSpecs.map(spec => {
|
|
const definition = findDefinition(runtimeSource, spec.sourceName, spec.start - 1)
|
|
if (definition.start !== spec.start || definition.end !== spec.end) {
|
|
throw new Error(`${spec.sourceName} UTF-16 range changed: expected [${spec.start}, ${spec.end}), found [${definition.start}, ${definition.end})`)
|
|
}
|
|
|
|
const body = localFunctionBody(localIcons, spec.localName)
|
|
const rangeNote = `UTF-16 [${definition.start}, ${definition.end})`
|
|
if (!localIcons.includes(rangeNote)) throw new Error(`${spec.localName} is missing its REA source range comment`)
|
|
const missingGeometry = sourceGeometry(definition.text).filter(value => !body.includes(value))
|
|
if (missingGeometry.length) throw new Error(`${spec.localName} does not match ${spec.sourceName}: ${missingGeometry.join(' | ')}`)
|
|
return {
|
|
sourceName: spec.sourceName,
|
|
localName: spec.localName,
|
|
utf16Range: [definition.start, definition.end],
|
|
definitionSha256: sha256(Buffer.from(definition.text, 'utf8'))
|
|
}
|
|
})
|
|
|
|
const questionSource = await readFile(questionPath, 'utf8')
|
|
const footerOffset = runtimeSource.indexOf('QuestionFloatingFooter=')
|
|
const footerSource = runtimeSource.slice(footerOffset, footerOffset + 1200)
|
|
if (footerOffset < 0 || !footerSource.includes('j=s?SendUpIcon:RightArrowSubmitIcon')) {
|
|
throw new Error('REA QuestionFloatingFooter no longer selects SendUpIcon for the last question')
|
|
}
|
|
if (!questionSource.includes('QuestionSendUpIcon') || !questionSource.includes('<QuestionSendUpIcon />') || questionSource.includes('RuntimeSendIcon')) {
|
|
throw new Error('QuestionFloating must use the recovered QuestionSendUpIcon for its send action')
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
runtimeEvidenceId,
|
|
runtimeFile,
|
|
runtimeSha256,
|
|
footerOffset,
|
|
footerSelection: 'j=s?SendUpIcon:RightArrowSubmitIcon',
|
|
recovered,
|
|
}, null, 2))
|