129 lines
7.0 KiB
JavaScript
129 lines
7.0 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
import { resolve } from 'node:path'
|
|
|
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
|
const runtimePath = '.cache/freecad/reference-desktop.json'
|
|
const sourcePaths = [
|
|
'.cache/freecad/FreeCAD/src/App/PropertyFile.h',
|
|
'.cache/freecad/FreeCAD/src/App/PropertyFile.cpp',
|
|
'.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h',
|
|
'.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp',
|
|
'.cache/freecad/FreeCAD/src/App/DocumentObjectFileIncluded.cpp',
|
|
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSection.cpp',
|
|
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawGeomHatch.cpp',
|
|
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawHatch.cpp',
|
|
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawSVGTemplate.cpp',
|
|
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawTileWeld.cpp',
|
|
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewImage.cpp',
|
|
]
|
|
const outputPath = resolve(root, 'config/freecad-property-fileincluded-inventory.json')
|
|
const [runtimeContent, ...sourceContents] = await Promise.all([
|
|
readFile(resolve(root, runtimePath)),
|
|
...sourcePaths.map((path) => readFile(resolve(root, path))),
|
|
])
|
|
const runtime = JSON.parse(runtimeContent)
|
|
const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded inventory generation failed: ${message}`) }
|
|
|
|
if (runtime.schemaVersion !== 1 || runtime.freecadVersion !== '1.1.1' || runtime.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
|
|
|
|
const dependencySources = new Map([
|
|
['TechDraw::DrawComplexSection.PatIncluded', 'FileGeomPattern'],
|
|
['TechDraw::DrawComplexSection.SvgIncluded', 'FileHatchPattern'],
|
|
['TechDraw::DrawComplexSectionPython.PatIncluded', 'FileGeomPattern'],
|
|
['TechDraw::DrawComplexSectionPython.SvgIncluded', 'FileHatchPattern'],
|
|
['TechDraw::DrawGeomHatch.PatIncluded', 'FilePattern'],
|
|
['TechDraw::DrawHatch.SvgIncluded', 'HatchPattern'],
|
|
['TechDraw::DrawSVGTemplate.PageResult', 'Template'],
|
|
['TechDraw::DrawTileWeld.SymbolIncluded', 'SymbolFile'],
|
|
['TechDraw::DrawTileWeldPython.SymbolIncluded', 'SymbolFile'],
|
|
['TechDraw::DrawViewImage.ImageIncluded', 'ImageFile'],
|
|
['TechDraw::DrawViewSection.PatIncluded', 'FileGeomPattern'],
|
|
['TechDraw::DrawViewSection.SvgIncluded', 'FileHatchPattern'],
|
|
['TechDraw::DrawViewSectionPython.PatIncluded', 'FileGeomPattern'],
|
|
['TechDraw::DrawViewSectionPython.SvgIncluded', 'FileHatchPattern'],
|
|
])
|
|
const normalizeStatus = (status) => status.map((entry) => entry === 27 ? 'PropOutput' : String(entry))
|
|
const records = []
|
|
for (const objectType of runtime.runtimeObjects?.types ?? []) {
|
|
for (const property of objectType.properties ?? []) {
|
|
if (property.typeId !== 'App::PropertyFileIncluded') continue
|
|
const key = `${objectType.typeId}.${property.name}`
|
|
const sourceProperty = dependencySources.get(key)
|
|
records.push({
|
|
objectTypeId: objectType.typeId,
|
|
objectAvailable: objectType.available === true,
|
|
probeStatus: objectType.probeStatus,
|
|
propertyName: property.name,
|
|
group: property.group,
|
|
status: property.status,
|
|
normalizedStatus: normalizeStatus(property.status),
|
|
defaultRaw: property.default,
|
|
defaultKind: property.default === '' ? 'empty' : 'document-transient-copy',
|
|
valueModel: {
|
|
kind: 'included-file-resource',
|
|
representation: 'read-only document-transient file backed by embedded FCStd bytes',
|
|
writableByStatus: property.status.length === 0,
|
|
pathIsRuntimeOnly: true,
|
|
bytesAreAuthoritative: true,
|
|
},
|
|
inputs: {
|
|
accepted: ['utf8-string-path', 'bytes-path', 'open-io-file', '[source-path,archive-name]', '{filename:string,filter?:string}'],
|
|
sourceFileMustExist: true,
|
|
emptyStringBehavior: 'accepted-no-op; does not clear an existing value',
|
|
sameTransientPathBehavior: 'rejected',
|
|
},
|
|
dependency: sourceProperty ? {
|
|
sourceProperty,
|
|
relation: 'host-onChanged-copies-source-bytes-into-included-property',
|
|
} : null,
|
|
editor: 'Gui::PropertyEditor::PropertyTransientFileItem',
|
|
applicability: {
|
|
requiredObjectTypeId: objectType.typeId,
|
|
propertyWriteRequiresShape: false,
|
|
source: 'Document.supportedTypes runtime inventory',
|
|
},
|
|
})
|
|
}
|
|
}
|
|
records.sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`))
|
|
if (records.length !== 20) fail(`expected 20 native App::PropertyFileIncluded records, found ${records.length}`)
|
|
if (new Set(records.map(({ objectTypeId }) => objectTypeId)).size !== 15) fail('expected 15 native host object types')
|
|
if (records.filter(({ valueModel }) => valueModel.writableByStatus).length !== 11 || records.filter(({ dependency }) => dependency !== null).length !== 14) fail('status or dependency partition changed')
|
|
|
|
const report = {
|
|
schemaVersion: 1,
|
|
status: 'pass',
|
|
baseline: { freecadVersion: runtime.freecadVersion, commit: runtime.gitCommit },
|
|
propertyType: 'App::PropertyFileIncluded',
|
|
recordCount: records.length,
|
|
objectTypeCount: new Set(records.map(({ objectTypeId }) => objectTypeId)).size,
|
|
records,
|
|
setterSemantics: {
|
|
externalSource: 'copy bytes into the document transient directory',
|
|
writableTransientSource: 'rename into the selected transient destination',
|
|
replacement: 'delete the previous transient copy unless undo owns it',
|
|
destinationPermissions: 'read-only',
|
|
archiveNameCollision: 'append a positive integer before the extension',
|
|
undoRedo: 'Copy/Paste move or copy transient files while preserving per-property ownership',
|
|
},
|
|
storage: {
|
|
xmlElement: 'FileIncluded',
|
|
archiveAttribute: 'file',
|
|
archiveResource: 'separate FCStd ZIP entry containing byte-identical file data',
|
|
forceXmlAttribute: 'data',
|
|
forceXmlResource: 'binary data embedded in the XML stream',
|
|
emptyEncoding: '<FileIncluded file=""/>',
|
|
restoredValue: 'document transient path; not a portable semantic path',
|
|
browserBoundary: 'project resource identity plus bytes; never expose or persist an ambient host path',
|
|
},
|
|
provenance: {
|
|
runtime: { path: runtimePath, bytes: runtimeContent.length, sha256: createHash('sha256').update(runtimeContent).digest('hex') },
|
|
sources: sourcePaths.map((path, index) => ({ path, bytes: sourceContents[index].length, sha256: createHash('sha256').update(sourceContents[index]).digest('hex') })),
|
|
},
|
|
classification: 'opaque-fcstd-proxy',
|
|
nextPhases: ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
|
|
}
|
|
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
|
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-inventory-generated', output: 'config/freecad-property-fileincluded-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount, objectTypeCount: report.objectTypeCount, writableByStatus: records.filter(({ valueModel }) => valueModel.writableByStatus).length, derivedRecords: records.filter(({ dependency }) => dependency !== null).length }, null, 2))
|