feat: add candidate FreeCAD naming SDK and attachment oracles
Build and verify the candidate-only FreeCAD naming bridge and OCCT worker path, including three-stage StringHasher restoration. Add Datum, ShapeBinder, attachment-mode, and PartDesign structure oracles plus offline SDK build plans and CI boundary checks.
This commit is contained in:
783
native/freecad-naming-bridge/freecad_naming_bridge.cpp
Normal file
783
native/freecad-naming-bridge/freecad_naming_bridge.cpp
Normal file
@@ -0,0 +1,783 @@
|
||||
#include <emscripten/bind.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <App/ElementMap.h>
|
||||
#include <App/ElementNamingUtils.h>
|
||||
#include <App/IndexedName.h>
|
||||
#include <App/MappedName.h>
|
||||
#include <App/StringHasher.h>
|
||||
#include <Base/Exception.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <pwd.h>
|
||||
#include <limits.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern "C" ssize_t readlink(const char* path, char* buffer, size_t size)
|
||||
{
|
||||
constexpr const char* executable = "/freecad/bin/freecad-naming-bridge";
|
||||
if (std::strcmp(path, "/proc/self/exe") != 0) {
|
||||
return -1;
|
||||
}
|
||||
const size_t length = std::strlen(executable);
|
||||
if (size < length) {
|
||||
return -1;
|
||||
}
|
||||
std::memcpy(buffer, executable, length);
|
||||
return static_cast<ssize_t>(length);
|
||||
}
|
||||
|
||||
extern "C" char* realpath(const char* path, char* resolvedPath)
|
||||
{
|
||||
static constexpr const char* allowed[] = {
|
||||
"/freecad-user",
|
||||
"/freecad-user/config",
|
||||
"/freecad-user/data",
|
||||
"/freecad-user/cache",
|
||||
"/freecad-user/temp",
|
||||
};
|
||||
if (!path || std::none_of(std::begin(allowed), std::end(allowed), [path](const char* entry) {
|
||||
return std::strcmp(path, entry) == 0;
|
||||
})) {
|
||||
return nullptr;
|
||||
}
|
||||
const size_t length = std::strlen(path);
|
||||
if (length >= PATH_MAX) {
|
||||
return nullptr;
|
||||
}
|
||||
char* output = resolvedPath ? resolvedPath : static_cast<char*>(std::malloc(length + 1));
|
||||
if (!output) {
|
||||
return nullptr;
|
||||
}
|
||||
std::memcpy(output, path, length + 1);
|
||||
return output;
|
||||
}
|
||||
|
||||
extern "C" int getpwuid_r(uid_t uid,
|
||||
struct passwd* password,
|
||||
char* buffer,
|
||||
size_t bufferSize,
|
||||
struct passwd** result)
|
||||
{
|
||||
constexpr const char identity[] = "freecad\0/freecad-user\0/bin/false";
|
||||
if (bufferSize < sizeof(identity)) {
|
||||
*result = nullptr;
|
||||
return -1;
|
||||
}
|
||||
std::memcpy(buffer, identity, sizeof(identity));
|
||||
password->pw_name = buffer;
|
||||
password->pw_passwd = const_cast<char*>("");
|
||||
password->pw_uid = uid;
|
||||
password->pw_gid = getgid();
|
||||
password->pw_gecos = buffer;
|
||||
password->pw_dir = buffer + sizeof("freecad");
|
||||
password->pw_shell = password->pw_dir + sizeof("/freecad-user");
|
||||
*result = password;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int ABI_VERSION = 1;
|
||||
constexpr const char* FREECAD_VERSION = "1.1.1";
|
||||
constexpr const char* FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d";
|
||||
|
||||
void ensureApplication()
|
||||
{
|
||||
static bool initialized = false;
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
char executable[] = "freecad-naming-bridge";
|
||||
char console[] = "--console";
|
||||
char* argv[] = {executable, console};
|
||||
App::Application::init(2, argv);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
QJsonObject requireObject(const QJsonObject& object, const char* key)
|
||||
{
|
||||
const QJsonValue value = object.value(key);
|
||||
if (!value.isObject()) {
|
||||
throw std::runtime_error(std::string("FreeCAD naming request requires object ") + key);
|
||||
}
|
||||
return value.toObject();
|
||||
}
|
||||
|
||||
QString requireString(const QJsonObject& object, const char* key)
|
||||
{
|
||||
const QJsonValue value = object.value(key);
|
||||
if (!value.isString() || value.toString().isEmpty()) {
|
||||
throw std::runtime_error(std::string("FreeCAD naming request requires string ") + key);
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
long requirePositiveTag(const QJsonObject& object, const char* key)
|
||||
{
|
||||
const double value = object.value(key).toDouble(-1.0);
|
||||
if (!std::isfinite(value) || value <= 0.0 || std::floor(value) != value
|
||||
|| value > static_cast<double>(0x7fffffff)) {
|
||||
throw std::runtime_error(std::string("FreeCAD naming request requires positive tag ") + key);
|
||||
}
|
||||
return static_cast<long>(value);
|
||||
}
|
||||
|
||||
int requireIndex(const QJsonObject& object, const char* key)
|
||||
{
|
||||
const double value = object.value(key).toDouble(-1.0);
|
||||
if (!std::isfinite(value) || value < 0.0 || std::floor(value) != value
|
||||
|| value > static_cast<double>(0x7ffffffe)) {
|
||||
throw std::runtime_error(std::string("FreeCAD naming request requires non-negative index ")
|
||||
+ key);
|
||||
}
|
||||
return static_cast<int>(value);
|
||||
}
|
||||
|
||||
QString titleKind(const QString& value)
|
||||
{
|
||||
if (value == "face") {
|
||||
return "Face";
|
||||
}
|
||||
if (value == "edge") {
|
||||
return "Edge";
|
||||
}
|
||||
if (value == "vertex") {
|
||||
return "Vertex";
|
||||
}
|
||||
throw std::runtime_error("FreeCAD naming history supports only face, edge, and vertex");
|
||||
}
|
||||
|
||||
QString canonicalOperation(const QString& operation)
|
||||
{
|
||||
QString output;
|
||||
output.reserve(operation.size());
|
||||
for (const QChar character : operation) {
|
||||
if (character.isLetterOrNumber()) {
|
||||
output.append(character.toUpper());
|
||||
}
|
||||
else if (character == '-') {
|
||||
output.append('_');
|
||||
}
|
||||
else {
|
||||
throw std::runtime_error("FreeCAD naming operation contains an unsupported character");
|
||||
}
|
||||
}
|
||||
if (output.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming operation is empty");
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
QJsonObject parseNameToken(const std::string& raw)
|
||||
{
|
||||
if (raw.empty() || (raw[0] != ':' && raw[0] != '$' && raw[0] != ';')) {
|
||||
throw std::runtime_error("FreeCAD ElementMap emitted an invalid name token");
|
||||
}
|
||||
std::vector<std::string> parts;
|
||||
std::size_t start = 0;
|
||||
while (true) {
|
||||
const std::size_t dot = raw.find('.', start);
|
||||
parts.push_back(raw.substr(start, dot == std::string::npos ? dot : dot - start));
|
||||
if (dot == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
start = dot + 1;
|
||||
}
|
||||
const QString marker = QString(QChar(raw[0]));
|
||||
QJsonObject token {{"raw", QString::fromStdString(raw)}, {"marker", marker}};
|
||||
QJsonArray suffix;
|
||||
if (raw[0] == ':') {
|
||||
if (parts.size() < 3) {
|
||||
throw std::runtime_error("FreeCAD ElementMap emitted a truncated indexed token");
|
||||
}
|
||||
bool postfixOk = false;
|
||||
bool indexOk = false;
|
||||
const int postfixIndex = QString::fromStdString(parts[0].substr(1)).toInt(&postfixOk, 16);
|
||||
const int elementIndex = QString::fromStdString(parts[1]).toInt(&indexOk, 16);
|
||||
if (!postfixOk || !indexOk) {
|
||||
throw std::runtime_error("FreeCAD ElementMap emitted a malformed indexed token");
|
||||
}
|
||||
token.insert("postfixIndex", postfixIndex);
|
||||
token.insert("elementIndex", elementIndex);
|
||||
for (std::size_t index = 2; index < parts.size(); ++index) {
|
||||
suffix.append(QString::fromStdString(parts[index]));
|
||||
}
|
||||
}
|
||||
else {
|
||||
token.insert("name", QString::fromStdString(parts[0].substr(1)));
|
||||
for (std::size_t index = 1; index < parts.size(); ++index) {
|
||||
suffix.append(QString::fromStdString(parts[index]));
|
||||
}
|
||||
}
|
||||
token.insert("suffix", suffix);
|
||||
return token;
|
||||
}
|
||||
|
||||
QJsonObject parseElementMap(const std::string& saved)
|
||||
{
|
||||
std::istringstream stream(saved);
|
||||
unsigned rootId = 0;
|
||||
int postfixCount = 0;
|
||||
std::string label;
|
||||
if (!(stream >> rootId >> label >> postfixCount) || rootId == 0 || label != "PostfixCount"
|
||||
|| postfixCount < 0) {
|
||||
throw std::runtime_error("FreeCAD ElementMap root header is invalid");
|
||||
}
|
||||
QJsonArray postfixes;
|
||||
for (int index = 0; index < postfixCount; ++index) {
|
||||
std::string postfix;
|
||||
if (!(stream >> postfix)) {
|
||||
throw std::runtime_error("FreeCAD ElementMap postfix list is truncated");
|
||||
}
|
||||
postfixes.append(QString::fromStdString(postfix));
|
||||
}
|
||||
int mapCount = 0;
|
||||
if (!(stream >> label >> mapCount) || label != "MapCount" || mapCount <= 0) {
|
||||
throw std::runtime_error("FreeCAD ElementMap map count is invalid");
|
||||
}
|
||||
QJsonArray maps;
|
||||
int rootMapIndex = 0;
|
||||
for (int mapOrdinal = 0; mapOrdinal < mapCount; ++mapOrdinal) {
|
||||
int mapIndex = 0;
|
||||
unsigned mapId = 0;
|
||||
int typeCount = 0;
|
||||
if (!(stream >> label >> mapIndex >> mapId >> typeCount) || label != "ElementMap"
|
||||
|| mapIndex <= 0 || typeCount < 0) {
|
||||
throw std::runtime_error("FreeCAD ElementMap map header is invalid");
|
||||
}
|
||||
rootMapIndex = std::max(rootMapIndex, mapIndex);
|
||||
QJsonArray sections;
|
||||
for (int typeOrdinal = 0; typeOrdinal < typeCount; ++typeOrdinal) {
|
||||
std::string sectionName;
|
||||
int childCount = 0;
|
||||
if (!(stream >> sectionName >> label >> childCount) || label != "ChildCount"
|
||||
|| childCount != 0) {
|
||||
throw std::runtime_error("Candidate bridge does not accept child ElementMap records");
|
||||
}
|
||||
int nameCount = 0;
|
||||
if (!(stream >> label >> nameCount) || label != "NameCount" || nameCount < 0) {
|
||||
throw std::runtime_error("FreeCAD ElementMap name count is invalid");
|
||||
}
|
||||
QJsonArray names;
|
||||
for (int nameOrdinal = 0; nameOrdinal < nameCount; ++nameOrdinal) {
|
||||
QJsonArray tokens;
|
||||
std::string raw;
|
||||
std::string token;
|
||||
while (stream >> token) {
|
||||
if (token == "0") {
|
||||
break;
|
||||
}
|
||||
if (!raw.empty()) {
|
||||
raw += ' ';
|
||||
}
|
||||
raw += token;
|
||||
tokens.append(parseNameToken(token));
|
||||
}
|
||||
if (!stream) {
|
||||
throw std::runtime_error("FreeCAD ElementMap name entry is truncated");
|
||||
}
|
||||
names.append(QJsonObject {
|
||||
{"tokens", tokens},
|
||||
{"trailing", "0"},
|
||||
{"raw", QString::fromStdString(raw.empty() ? "0" : raw + " 0")},
|
||||
});
|
||||
}
|
||||
sections.append(QJsonObject {
|
||||
{"name", QString::fromStdString(sectionName)},
|
||||
{"children", QJsonArray {}},
|
||||
{"names", names},
|
||||
});
|
||||
}
|
||||
if (!(stream >> label) || label != "EndMap") {
|
||||
throw std::runtime_error("FreeCAD ElementMap map terminator is missing");
|
||||
}
|
||||
maps.append(QJsonObject {
|
||||
{"index", mapIndex},
|
||||
{"id", static_cast<qint64>(mapId)},
|
||||
{"typeCount", typeCount},
|
||||
{"sections", sections},
|
||||
});
|
||||
}
|
||||
return QJsonObject {
|
||||
{"schemaVersion", 2},
|
||||
{"nativeVersion", 1},
|
||||
{"rootId", static_cast<qint64>(rootId)},
|
||||
{"postfixes", postfixes},
|
||||
{"maps", maps},
|
||||
{"rootMapIndex", rootMapIndex},
|
||||
};
|
||||
}
|
||||
|
||||
QJsonObject stringHasherJson(const App::StringHasherRef& hasher)
|
||||
{
|
||||
QJsonArray entries;
|
||||
for (const auto& [id, reference] : hasher->getIDMap()) {
|
||||
const App::StringID& stringId = reference.deref();
|
||||
int flags = 0;
|
||||
flags |= stringId.isBinary() ? 1 << 0 : 0;
|
||||
flags |= stringId.isHashed() ? 1 << 1 : 0;
|
||||
flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0;
|
||||
flags |= stringId.isPostfixed() ? 1 << 3 : 0;
|
||||
flags |= stringId.isIndexed() ? 1 << 4 : 0;
|
||||
flags |= stringId.isPrefixID() ? 1 << 5 : 0;
|
||||
flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0;
|
||||
flags |= stringId.isPersistent() ? 1 << 7 : 0;
|
||||
QJsonArray relatedIds;
|
||||
for (const auto& related : reference.relatedIDs()) {
|
||||
relatedIds.append(static_cast<qint64>(related.value()));
|
||||
}
|
||||
entries.append(QJsonObject {
|
||||
{"id", static_cast<qint64>(id)},
|
||||
{"flags", flags},
|
||||
{"relatedIds", relatedIds},
|
||||
{"data", QString::fromUtf8(stringId.data())},
|
||||
{"postfix", QString::fromUtf8(stringId.postfix())},
|
||||
});
|
||||
}
|
||||
return QJsonObject {{"schemaVersion", 2}, {"nativeVersion", 1}, {"entries", entries}};
|
||||
}
|
||||
|
||||
int stringIdFlags(const App::StringID& stringId)
|
||||
{
|
||||
int flags = 0;
|
||||
flags |= stringId.isBinary() ? 1 << 0 : 0;
|
||||
flags |= stringId.isHashed() ? 1 << 1 : 0;
|
||||
flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0;
|
||||
flags |= stringId.isPostfixed() ? 1 << 3 : 0;
|
||||
flags |= stringId.isIndexed() ? 1 << 4 : 0;
|
||||
flags |= stringId.isPrefixID() ? 1 << 5 : 0;
|
||||
flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0;
|
||||
flags |= stringId.isPersistent() ? 1 << 7 : 0;
|
||||
return flags;
|
||||
}
|
||||
|
||||
long requireStringId(const QJsonValue& value, const char* label)
|
||||
{
|
||||
const double number = value.toDouble(-1.0);
|
||||
if (!std::isfinite(number) || number <= 0.0 || std::floor(number) != number
|
||||
|| number > static_cast<double>(0x7fffffff)) {
|
||||
throw std::runtime_error(std::string("Prior StringHasher requires positive ") + label);
|
||||
}
|
||||
return static_cast<long>(number);
|
||||
}
|
||||
|
||||
void restoreStringHasherTable(const QJsonObject& table, const App::StringHasherRef& hasher)
|
||||
{
|
||||
if (table.value("schemaVersion").toInt() != 2
|
||||
|| table.value("nativeVersion").toInt() != 1) {
|
||||
throw std::runtime_error("Prior StringHasher schema or native version is unsupported");
|
||||
}
|
||||
const QJsonArray entries = table.value("entries").toArray();
|
||||
if (entries.size() > 1000000) {
|
||||
throw std::runtime_error("Prior StringHasher entry count exceeds the candidate limit");
|
||||
}
|
||||
long previousId = 0;
|
||||
for (const QJsonValue entryValue : entries) {
|
||||
if (!entryValue.isObject()) {
|
||||
throw std::runtime_error("Prior StringHasher entry must be an object");
|
||||
}
|
||||
const QJsonObject entry = entryValue.toObject();
|
||||
const long id = requireStringId(entry.value("id"), "entry ID");
|
||||
if (id != previousId + 1) {
|
||||
throw std::runtime_error("Prior StringHasher IDs must be contiguous and ordered");
|
||||
}
|
||||
const int flags = entry.value("flags").toInt(-1);
|
||||
constexpr int knownFlags = 0xff;
|
||||
if (flags < 0 || (flags & ~knownFlags) != 0) {
|
||||
throw std::runtime_error("Prior StringHasher entry contains unknown flags");
|
||||
}
|
||||
if ((flags & ((1 << 0) | (1 << 1))) != 0) {
|
||||
throw std::runtime_error(
|
||||
"Candidate bridge cannot losslessly restore binary or one-way-hashed StringHasher entries");
|
||||
}
|
||||
if (!entry.value("data").isString() || !entry.value("postfix").isString()
|
||||
|| !entry.value("relatedIds").isArray()) {
|
||||
throw std::runtime_error("Prior StringHasher entry payload is incomplete");
|
||||
}
|
||||
const QByteArray data = entry.value("data").toString().toUtf8();
|
||||
const QByteArray postfix = entry.value("postfix").toString().toUtf8();
|
||||
const QJsonArray relatedValues = entry.value("relatedIds").toArray();
|
||||
std::vector<long> relatedIds;
|
||||
relatedIds.reserve(relatedValues.size());
|
||||
for (const QJsonValue relatedValue : relatedValues) {
|
||||
const long relatedId = requireStringId(relatedValue, "related ID");
|
||||
if (relatedId >= id || !hasher->getID(relatedId)) {
|
||||
throw std::runtime_error("Prior StringHasher related ID is unresolved or forward-referenced");
|
||||
}
|
||||
relatedIds.push_back(relatedId);
|
||||
}
|
||||
|
||||
App::StringIDRef restored;
|
||||
if ((flags & (1 << 3)) == 0) {
|
||||
if (flags != 0 && flags != (1 << 7)) {
|
||||
throw std::runtime_error("Prior non-postfixed StringHasher flags cannot be reconstructed");
|
||||
}
|
||||
restored = hasher->getID(data, App::StringHasher::Option::None);
|
||||
}
|
||||
else {
|
||||
QByteArray mappedData = data;
|
||||
if ((flags & ((1 << 4) | (1 << 6))) != 0) {
|
||||
mappedData += '1';
|
||||
}
|
||||
Data::MappedName mapped(mappedData);
|
||||
if (!postfix.isEmpty()) {
|
||||
mapped += postfix;
|
||||
}
|
||||
int internalCount = 0;
|
||||
internalCount += (flags & (1 << 2)) != 0 ? 1 : 0;
|
||||
internalCount += (flags & (1 << 4)) != 0 ? 1 : 0;
|
||||
if (internalCount > static_cast<int>(relatedIds.size())) {
|
||||
throw std::runtime_error("Prior StringHasher internal dependencies are truncated");
|
||||
}
|
||||
Data::ElementIDRefs externalRefs;
|
||||
for (std::size_t index = static_cast<std::size_t>(internalCount);
|
||||
index < relatedIds.size(); ++index) {
|
||||
externalRefs.push_back(hasher->getID(relatedIds[index]));
|
||||
}
|
||||
restored = hasher->getID(mapped, externalRefs);
|
||||
}
|
||||
if (!restored || restored.value() != id) {
|
||||
throw std::runtime_error("Prior StringHasher entry did not restore to its persistent ID");
|
||||
}
|
||||
if ((flags & (1 << 7)) != 0) {
|
||||
restored.setPersistent(true);
|
||||
}
|
||||
const App::StringID& actual = restored.deref();
|
||||
std::vector<long> actualRelated;
|
||||
for (const App::StringIDRef& related : actual.relatedIDs()) {
|
||||
actualRelated.push_back(related.value());
|
||||
}
|
||||
if (stringIdFlags(actual) != flags || actual.data() != data || actual.postfix() != postfix
|
||||
|| actualRelated != relatedIds) {
|
||||
throw std::runtime_error("Prior StringHasher entry changed during strict restoration");
|
||||
}
|
||||
previousId = id;
|
||||
}
|
||||
}
|
||||
|
||||
void restorePriorStringHasher(const QJsonArray& inputs, const App::StringHasherRef& hasher)
|
||||
{
|
||||
QByteArray canonical;
|
||||
QJsonObject selected;
|
||||
for (const QJsonValue inputValue : inputs) {
|
||||
const QJsonObject evidence = inputValue.toObject().value("namingEvidence").toObject();
|
||||
if (evidence.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonValue tableValue = evidence.value("stringHasher");
|
||||
if (!tableValue.isObject()) {
|
||||
const QJsonArray mappedNames = evidence.value("mappedNames").toArray();
|
||||
const bool requiresHasher = std::any_of(
|
||||
mappedNames.begin(), mappedNames.end(), [](const QJsonValue& mappedValue) {
|
||||
const QJsonObject reference = mappedValue.toObject().value("reference").toObject();
|
||||
return !reference.value("stringIds").toArray().isEmpty()
|
||||
|| reference.contains("prefixStringId");
|
||||
});
|
||||
if (requiresHasher) {
|
||||
throw std::runtime_error("Prior mapped-name IDs require StringHasher evidence");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const QJsonObject table = tableValue.toObject();
|
||||
const QByteArray encoded = QJsonDocument(table).toJson(QJsonDocument::Compact);
|
||||
if (canonical.isEmpty()) {
|
||||
canonical = encoded;
|
||||
selected = table;
|
||||
}
|
||||
else if (canonical != encoded) {
|
||||
throw std::runtime_error("Candidate bridge cannot merge inconsistent prior StringHasher tables");
|
||||
}
|
||||
}
|
||||
if (!selected.isEmpty()) {
|
||||
restoreStringHasherTable(selected, hasher);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject findSourceInput(const QJsonArray& inputs, const QString& source)
|
||||
{
|
||||
for (const QJsonValue value : inputs) {
|
||||
const QJsonObject input = value.toObject();
|
||||
if (input.value("inputId").toString() == source || input.value("role").toString() == source) {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
if (source == "object" && !inputs.isEmpty()) {
|
||||
return inputs.first().toObject();
|
||||
}
|
||||
if (source == "tool" && inputs.size() > 1) {
|
||||
return inputs.at(1).toObject();
|
||||
}
|
||||
throw std::runtime_error("FreeCAD naming history references an unknown input");
|
||||
}
|
||||
|
||||
struct SourceName
|
||||
{
|
||||
Data::MappedName name;
|
||||
QString persistentId;
|
||||
};
|
||||
|
||||
SourceName sourceNameFor(const QJsonObject& input,
|
||||
const QString& kind,
|
||||
int sourceIndex,
|
||||
const App::StringHasherRef& hasher)
|
||||
{
|
||||
const QString indexed = titleKind(kind) + QString::number(sourceIndex + 1);
|
||||
const QJsonValue evidenceValue = input.value("namingEvidence");
|
||||
if (!evidenceValue.isObject()) {
|
||||
return {Data::MappedName(indexed.toStdString()), indexed};
|
||||
}
|
||||
const QJsonArray mappedNames = evidenceValue.toObject().value("mappedNames").toArray();
|
||||
for (const QJsonValue value : mappedNames) {
|
||||
const QJsonObject mapped = value.toObject();
|
||||
if (mapped.value("kind").toString() != kind
|
||||
|| mapped.value("resultIndex").toInt(-1) != sourceIndex) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject reference = requireObject(mapped, "reference");
|
||||
const QJsonArray stringIds = reference.value("stringIds").toArray();
|
||||
for (const QJsonValue idValue : stringIds) {
|
||||
if (!hasher->getID(requireStringId(idValue, "mapped-name string ID"))) {
|
||||
throw std::runtime_error("Prior mapped-name string ID is absent from StringHasher");
|
||||
}
|
||||
}
|
||||
if (reference.contains("prefixStringId")) {
|
||||
const long prefixId = requireStringId(reference.value("prefixStringId"),
|
||||
"mapped-name prefix ID");
|
||||
if (!hasher->getID(prefixId)) {
|
||||
throw std::runtime_error("Prior mapped-name prefix ID is absent from StringHasher");
|
||||
}
|
||||
}
|
||||
Data::MappedName name(requireString(reference, "name").toStdString());
|
||||
const QString postfix = reference.value("postfix").toString();
|
||||
if (!postfix.isEmpty()) {
|
||||
name += postfix.toStdString();
|
||||
}
|
||||
return {name, mapped.value("resultPersistentId").toString(indexed)};
|
||||
}
|
||||
throw std::runtime_error("Prior naming evidence has no matching source subshape");
|
||||
}
|
||||
|
||||
QJsonObject mappedReference(const Data::MappedName& name, const Data::ElementIDRefs& stringIds)
|
||||
{
|
||||
QJsonArray ids;
|
||||
for (const App::StringIDRef& stringId : stringIds) {
|
||||
ids.append(static_cast<qint64>(stringId.value()));
|
||||
}
|
||||
QJsonObject reference {
|
||||
{"name", QString::fromUtf8(name.dataBytes())},
|
||||
{"postfix", QString::fromUtf8(name.postfixBytes())},
|
||||
{"stringIds", ids},
|
||||
};
|
||||
const App::StringID::IndexID prefix = App::StringID::fromString(name.dataBytes());
|
||||
if (prefix) {
|
||||
reference.insert("prefixStringId", static_cast<qint64>(prefix.id));
|
||||
}
|
||||
return reference;
|
||||
}
|
||||
|
||||
std::string evidenceJsonImpl(const std::string& requestJson)
|
||||
{
|
||||
ensureApplication();
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document =
|
||||
QJsonDocument::fromJson(QByteArray::fromStdString(requestJson), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
throw std::runtime_error("FreeCAD naming request is not valid JSON");
|
||||
}
|
||||
const QJsonObject request = document.object();
|
||||
if (request.value("schemaVersion").toInt() != ABI_VERSION) {
|
||||
throw std::runtime_error("FreeCAD naming request schemaVersion is unsupported");
|
||||
}
|
||||
const QString stageId = requireString(request, "stageId");
|
||||
const QString resultObjectId = requireString(request, "resultObjectId");
|
||||
const long resultTag = requirePositiveTag(request, "resultObjectTag");
|
||||
const QString operation = requireString(request, "operation");
|
||||
const QString operationPostfix = canonicalOperation(operation);
|
||||
const QJsonArray inputs = request.value("inputs").toArray();
|
||||
const QJsonObject history = requireObject(request, "history");
|
||||
const QJsonArray records = history.value("records").toArray();
|
||||
if (inputs.isEmpty() || records.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming request requires inputs and native history records");
|
||||
}
|
||||
|
||||
App::StringHasherRef hasher(new App::StringHasher());
|
||||
hasher->setSaveAll(true);
|
||||
restorePriorStringHasher(inputs, hasher);
|
||||
auto elementMap = std::make_shared<Data::ElementMap>();
|
||||
elementMap->hasher = hasher;
|
||||
QJsonArray mappedNames;
|
||||
std::set<std::string> resultKeys;
|
||||
std::map<std::string, int> sourceOrdinals;
|
||||
|
||||
for (const QJsonValue recordValue : records) {
|
||||
const QJsonObject record = recordValue.toObject();
|
||||
const QString relation = requireString(record, "relation");
|
||||
if (relation == "deleted") {
|
||||
continue;
|
||||
}
|
||||
if (relation != "modified" && relation != "generated") {
|
||||
throw std::runtime_error("FreeCAD naming history relation is unsupported");
|
||||
}
|
||||
const QString sourceKind = requireString(record, "kind");
|
||||
const QString resultKind = record.value("resultKind").toString(sourceKind);
|
||||
titleKind(sourceKind);
|
||||
const QString resultType = titleKind(resultKind);
|
||||
const int sourceIndex = requireIndex(record, "sourceIndex");
|
||||
const QString source = record.value("sourceId").toString(
|
||||
requireString(record, "source"));
|
||||
const QJsonObject input = findSourceInput(inputs, source);
|
||||
const QString sourceObjectId = requireString(input, "objectId");
|
||||
const long sourceTag = requirePositiveTag(input, "objectTag");
|
||||
const SourceName sourceName = sourceNameFor(input, sourceKind, sourceIndex, hasher);
|
||||
QJsonArray resultIndexes = record.value("resultIndexes").toArray();
|
||||
if (resultIndexes.isEmpty() && record.contains("resultIndex")) {
|
||||
resultIndexes.append(record.value("resultIndex"));
|
||||
}
|
||||
if (resultIndexes.isEmpty()) {
|
||||
throw std::runtime_error("Non-deleted FreeCAD naming history requires result indexes");
|
||||
}
|
||||
const std::string ordinalKey = (sourceObjectId + '|' + sourceKind + '|'
|
||||
+ QString::number(sourceIndex) + '|' + relation)
|
||||
.toStdString();
|
||||
for (const QJsonValue resultValue : resultIndexes) {
|
||||
const int resultIndex = requireIndex(QJsonObject {{"value", resultValue}}, "value");
|
||||
const std::string resultKey =
|
||||
(resultKind + ':' + QString::number(resultIndex)).toStdString();
|
||||
if (!resultKeys.insert(resultKey).second) {
|
||||
throw std::runtime_error("FreeCAD naming history maps the same result subshape twice");
|
||||
}
|
||||
const int ordinal = ++sourceOrdinals[ordinalKey];
|
||||
std::ostringstream postfix;
|
||||
postfix << (relation == "modified" ? Data::POSTFIX_MOD : Data::POSTFIX_GEN);
|
||||
if (ordinal > 1) {
|
||||
postfix << ordinal;
|
||||
}
|
||||
Data::MappedName encoded(sourceName.name);
|
||||
Data::ElementIDRefs stringIds;
|
||||
elementMap->encodeElementName(
|
||||
resultType.at(0).toLatin1(),
|
||||
encoded,
|
||||
postfix,
|
||||
&stringIds,
|
||||
resultTag,
|
||||
operationPostfix.toUtf8().constData(),
|
||||
sourceTag);
|
||||
const Data::IndexedName resultIndexed(
|
||||
(resultType + QString::number(resultIndex + 1)).toUtf8());
|
||||
const Data::MappedName stored =
|
||||
elementMap->setElementName(resultIndexed, encoded, resultTag, &stringIds);
|
||||
if (!stored) {
|
||||
throw std::runtime_error("FreeCAD ElementMap rejected a native history mapping");
|
||||
}
|
||||
mappedNames.append(QJsonObject {
|
||||
{"kind", resultKind},
|
||||
{"resultIndex", resultIndex},
|
||||
{"resultPersistentId", resultType + QString::number(resultIndex + 1)},
|
||||
{"relation", relation},
|
||||
{"reference", mappedReference(stored, stringIds)},
|
||||
{"sourceRefs", QJsonArray {QJsonObject {
|
||||
{"objectId", sourceObjectId},
|
||||
{"persistentId", sourceName.persistentId},
|
||||
{"stageId", input.value("stageId")},
|
||||
}}},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mappedNames.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming history has no result subshape evidence");
|
||||
}
|
||||
|
||||
elementMap->beforeSave(hasher);
|
||||
std::ostringstream saved;
|
||||
elementMap->save(saved);
|
||||
const QJsonObject response {
|
||||
{"schemaVersion", ABI_VERSION},
|
||||
{"stageId", stageId},
|
||||
{"resultObjectId", resultObjectId},
|
||||
{"status", "native-evidence"},
|
||||
{"mappedNames", mappedNames},
|
||||
{"stringHasher", stringHasherJson(hasher)},
|
||||
{"elementMap2", parseElementMap(saved.str())},
|
||||
};
|
||||
return QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
|
||||
std::string evidenceJson(const std::string& requestJson)
|
||||
{
|
||||
try {
|
||||
return evidenceJsonImpl(requestJson);
|
||||
}
|
||||
catch (const Base::Exception& exception) {
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", ABI_VERSION},
|
||||
{"status", "error"},
|
||||
{"error", QString::fromStdString("FreeCAD private naming failure: "
|
||||
+ exception.getMessage())},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
catch (const std::exception& exception) {
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", ABI_VERSION},
|
||||
{"status", "error"},
|
||||
{"error", QString::fromUtf8(exception.what())},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
catch (...) {
|
||||
return R"({"schemaVersion":1,"status":"error","error":"Unknown native exception"})";
|
||||
}
|
||||
}
|
||||
|
||||
int freecadNamingAbiVersion()
|
||||
{
|
||||
return ABI_VERSION;
|
||||
}
|
||||
|
||||
std::string freecadNamingCapabilitiesJson()
|
||||
{
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", ABI_VERSION},
|
||||
{"freecadVersion", FREECAD_VERSION},
|
||||
{"sourceCommit", FREECAD_COMMIT},
|
||||
{"mappedNameRef", true},
|
||||
{"stringHasher", true},
|
||||
{"elementMap2", true},
|
||||
{"operations", QJsonArray {
|
||||
"fuse", "cut", "common", "rotate", "pad", "pocket", "loft", "pipe",
|
||||
"revolution", "groove", "fillet", "chamfer", "hole", "draft", "thickness",
|
||||
"linear-pattern", "polar-pattern", "mirrored", "multi-transform",
|
||||
}},
|
||||
{"maxRequestBytes", 16 * 1024 * 1024},
|
||||
{"maxResponseBytes", 32 * 1024 * 1024},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
EMSCRIPTEN_BINDINGS(freecad_private_naming_bridge)
|
||||
{
|
||||
emscripten::function("freecadNamingAbiVersion", &freecadNamingAbiVersion);
|
||||
emscripten::function("freecadNamingCapabilitiesJson", &freecadNamingCapabilitiesJson);
|
||||
emscripten::function("freecadNamingEvidenceJson", &evidenceJson);
|
||||
}
|
||||
26
native/freecad-naming-bridge/pre.js
Normal file
26
native/freecad-naming-bridge/pre.js
Normal file
@@ -0,0 +1,26 @@
|
||||
Module.preRun = Module.preRun || [];
|
||||
Module.preRun.push(() => {
|
||||
for (const path of [
|
||||
'/freecad-user',
|
||||
'/freecad-user/config',
|
||||
'/freecad-user/data',
|
||||
'/freecad-user/cache',
|
||||
'/freecad-user/temp',
|
||||
]) {
|
||||
try {
|
||||
FS.mkdir(path)
|
||||
} catch (error) {
|
||||
if (!FS.analyzePath(path).exists) throw error
|
||||
}
|
||||
}
|
||||
Object.assign(ENV, {
|
||||
HOME: '/freecad-user',
|
||||
FREECAD_USER_HOME: '/freecad-user',
|
||||
FREECAD_USER_DATA: '/freecad-user/data',
|
||||
FREECAD_USER_TEMP: '/freecad-user/temp',
|
||||
XDG_CONFIG_HOME: '/freecad-user/config',
|
||||
XDG_DATA_HOME: '/freecad-user/data',
|
||||
XDG_CACHE_HOME: '/freecad-user/cache',
|
||||
TMPDIR: '/freecad-user/temp',
|
||||
})
|
||||
})
|
||||
185
native/freecad-naming-bridge/smoke-test.ts
Normal file
185
native/freecad-naming-bridge/smoke-test.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import createBridge from './dist/freecad-private-naming-bridge.js'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import {
|
||||
captureFreeCadPrivateNamingEvidence,
|
||||
createFreeCadPrivateNamingAbiRequest,
|
||||
probeFreeCadPrivateNamingAbi,
|
||||
type NativeFreeCadNamingAbiModule,
|
||||
} from '../../src/facade/nativeNamingAbi'
|
||||
|
||||
const distUrl = new URL('./dist/', import.meta.url)
|
||||
const bridge = await createBridge({ locateFile: (path: string) => fileURLToPath(new URL(path, distUrl)) }) as NativeFreeCadNamingAbiModule
|
||||
const probe = probeFreeCadPrivateNamingAbi(bridge)
|
||||
if (probe.availability !== 'available') throw new Error(`Isolated bridge ABI probe failed: ${probe.reason}`)
|
||||
|
||||
const request = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-1',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 1,
|
||||
operationId: 'isolated-cut-1',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:1',
|
||||
resultObjectId: 'isolated:result:1',
|
||||
resultObjectTag: 99,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'source-object', role: 'object', step: 'ISO-10303-21; object', objectTag: 42 },
|
||||
{ inputId: 'tool', objectId: 'source-tool', role: 'tool', step: 'ISO-10303-21; tool', objectTag: 43 },
|
||||
],
|
||||
stages: [{ stageId: 'isolated:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }],
|
||||
},
|
||||
})
|
||||
const first = captureFreeCadPrivateNamingEvidence(bridge, request, probe)
|
||||
if (!first || first.status !== 'native-evidence' || first.mappedNames?.length !== 1 || !first.elementMap2 || !first.stringHasher) throw new Error('Isolated bridge returned incomplete first-stage evidence.')
|
||||
if (!first.mappedNames[0].reference.postfix?.includes(';:M;CUT;:H2a:7,F')) throw new Error(`Unexpected first-stage FreeCAD postfix: ${first.mappedNames[0].reference.postfix}`)
|
||||
|
||||
const chained = captureFreeCadPrivateNamingEvidence(bridge, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-2',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 2,
|
||||
operationId: 'isolated-cut-2',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:2',
|
||||
resultObjectId: 'isolated:result:2',
|
||||
resultObjectTag: 100,
|
||||
inputs: [{ inputId: 'object', objectId: 'isolated:result:1', role: 'object', stageId: 'isolated:stage:1', step: 'ISO-10303-21; first result', objectTag: 99, namingEvidence: first }],
|
||||
stages: [{ stageId: 'isolated:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; second result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 1 }],
|
||||
},
|
||||
}), probe)
|
||||
if (!chained || chained.elementMap2?.maps.length !== 1) throw new Error('Isolated bridge did not preserve chained ElementMap2 evidence.')
|
||||
const chainedReference = chained.mappedNames?.[0].reference
|
||||
const prefixStringId = chainedReference?.prefixStringId
|
||||
const stringHasherIds = new Set(chained.stringHasher?.entries.map(({ id }) => id))
|
||||
const chainedTokens = chained.elementMap2.maps.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens)))
|
||||
if (!chainedReference?.name.startsWith('#') || !Number.isSafeInteger(prefixStringId) || !chainedReference.stringIds?.includes(prefixStringId!) || !stringHasherIds.has(prefixStringId!) || !chainedTokens.some((token) => token.marker === '$' && token.name === chainedReference.name)) throw new Error('Isolated bridge did not emit a closed native hashed MappedNameRef/StringHasher/ElementMap2 chain.')
|
||||
|
||||
const third = captureFreeCadPrivateNamingEvidence(bridge, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-3',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'isolated-cut-3',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:3',
|
||||
resultObjectId: 'isolated:result:3',
|
||||
resultObjectTag: 101,
|
||||
inputs: [{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained }],
|
||||
stages: [{ stageId: 'isolated:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; third result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }],
|
||||
},
|
||||
}), probe)
|
||||
const thirdReference = third?.mappedNames?.[0]?.reference
|
||||
const thirdHasherIds = new Set(third?.stringHasher?.entries.map(({ id }) => id))
|
||||
if (!third || third.status !== 'native-evidence' || !thirdReference?.name.startsWith('#') || !thirdReference.stringIds?.every((id) => thirdHasherIds.has(id)) || (third.stringHasher?.entries.length ?? 0) <= (chained.stringHasher?.entries.length ?? 0)) throw new Error('Isolated bridge did not restore prior StringHasher evidence for a third naming stage.')
|
||||
|
||||
let tamperedHasherRejected = false
|
||||
try {
|
||||
const validRequest = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-tampered',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'isolated-cut-tampered',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:tampered',
|
||||
resultObjectId: 'isolated:result:tampered',
|
||||
resultObjectTag: 102,
|
||||
inputs: [{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained }],
|
||||
stages: [{ stageId: 'isolated:stage:tampered', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; tampered result',
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }] },
|
||||
})
|
||||
const tamperedRequest = structuredClone(validRequest)
|
||||
const tamperedEntry = tamperedRequest.inputs[0].namingEvidence?.stringHasher?.entries[0]
|
||||
if (!tamperedEntry) throw new Error('Chained evidence has no StringHasher entry to tamper.')
|
||||
tamperedEntry.id += 1
|
||||
const response = JSON.parse(bridge.freecadNamingEvidenceJson!(JSON.stringify(tamperedRequest))) as { status?: string; error?: string }
|
||||
if (response.status !== 'error' || !response.error?.includes('StringHasher IDs must be contiguous and ordered')) throw new Error(`Unexpected tampered StringHasher response: ${JSON.stringify(response)}`)
|
||||
tamperedHasherRejected = true
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
if (!tamperedHasherRejected) throw new Error('Isolated bridge accepted tampered prior StringHasher evidence.')
|
||||
|
||||
let inconsistentTablesRejected = false
|
||||
{
|
||||
const inconsistentRequest = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-inconsistent',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'isolated-cut-inconsistent',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:inconsistent',
|
||||
resultObjectId: 'isolated:result:inconsistent',
|
||||
resultObjectTag: 103,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained },
|
||||
{ inputId: 'tool', objectId: 'isolated:result:2-copy', role: 'tool', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result copy', objectTag: 104, namingEvidence: chained },
|
||||
],
|
||||
stages: [{ stageId: 'isolated:stage:inconsistent', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; inconsistent result',
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }] },
|
||||
})
|
||||
const secondTable = inconsistentRequest.inputs[1].namingEvidence?.stringHasher
|
||||
if (!secondTable?.entries[0]) throw new Error('Chained evidence has no StringHasher table to make inconsistent.')
|
||||
secondTable.entries[0].data += '-different'
|
||||
const response = JSON.parse(bridge.freecadNamingEvidenceJson!(JSON.stringify(inconsistentRequest))) as { status?: string; error?: string }
|
||||
if (response.status !== 'error' || !response.error?.includes('cannot merge inconsistent prior StringHasher tables')) throw new Error(`Unexpected inconsistent StringHasher response: ${JSON.stringify(response)}`)
|
||||
inconsistentTablesRejected = true
|
||||
}
|
||||
|
||||
let rejected = false
|
||||
try {
|
||||
captureFreeCadPrivateNamingEvidence(bridge, { ...request, history: { ...request.history, records: [] } }, probe)
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error) || !error.message.includes('requires inputs and native history records')) throw error
|
||||
rejected = true
|
||||
}
|
||||
if (!rejected) throw new Error('Isolated bridge accepted a request without native history records.')
|
||||
|
||||
const result = {
|
||||
status: 'freecad-private-naming-isolated-bridge-pass',
|
||||
descriptor: probe.descriptor,
|
||||
firstStage: { mappedNames: first.mappedNames.length, stringHasherEntries: first.stringHasher.entries.length },
|
||||
chainedStage: { mappedNames: chained.mappedNames.length, stringHasherEntries: chained.stringHasher.entries.length },
|
||||
thirdStage: { mappedNames: third.mappedNames.length, stringHasherEntries: third.stringHasher.entries.length },
|
||||
tamperedHasherRejected,
|
||||
inconsistentTablesRejected,
|
||||
productionPublication: false,
|
||||
productionWorkerLinked: false,
|
||||
}
|
||||
const reportPath = process.env.FREECAD_NAMING_BRIDGE_REPORT
|
||||
if (reportPath) {
|
||||
const artifacts = await Promise.all(['freecad-private-naming-bridge.js', 'freecad-private-naming-bridge.wasm', 'freecad-private-naming-bridge.data'].map(async (name) => {
|
||||
const path = fileURLToPath(new URL(name, distUrl))
|
||||
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
|
||||
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
|
||||
}))
|
||||
const absoluteReportPath = resolve(reportPath)
|
||||
await mkdir(dirname(absoluteReportPath), { recursive: true })
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify({ schemaVersion: 1, ...result, artifacts }, null, 2)}\n`)
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
74
native/freecad-naming-probe/abi-candidate-smoke.ts
Normal file
74
native/freecad-naming-probe/abi-candidate-smoke.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import createProbe from './dist/freecad-private-naming-source-probe.js'
|
||||
import { parseElementMap2 } from '../../src/facade/elementMap2'
|
||||
import {
|
||||
captureFreeCadPrivateNamingEvidence,
|
||||
createFreeCadPrivateNamingAbiRequest,
|
||||
probeFreeCadPrivateNamingAbi,
|
||||
type NativeFreeCadNamingAbiModule,
|
||||
} from '../../src/facade/nativeNamingAbi'
|
||||
|
||||
type CandidateProbe = {
|
||||
freecadNamingCandidateAbiVersion(): number
|
||||
freecadNamingCandidateCapabilitiesJson(): string
|
||||
freecadNamingCandidateEvidenceJson(requestJson: string): string
|
||||
}
|
||||
|
||||
const native = await createProbe() as CandidateProbe
|
||||
const candidate: NativeFreeCadNamingAbiModule = {
|
||||
freecadNamingAbiVersion: () => native.freecadNamingCandidateAbiVersion(),
|
||||
freecadNamingCapabilitiesJson: () => native.freecadNamingCandidateCapabilitiesJson(),
|
||||
freecadNamingEvidenceJson: (requestJson) => {
|
||||
const response = JSON.parse(native.freecadNamingCandidateEvidenceJson(requestJson)) as Record<string, unknown>
|
||||
const elementMap2Text = response.elementMap2Text
|
||||
if (typeof elementMap2Text !== 'string') throw new TypeError('Candidate response omitted native ElementMap2 text.')
|
||||
delete response.elementMap2Text
|
||||
response.elementMap2 = parseElementMap2(elementMap2Text)
|
||||
return JSON.stringify(response)
|
||||
},
|
||||
}
|
||||
|
||||
const abiProbe = probeFreeCadPrivateNamingAbi(candidate)
|
||||
if (abiProbe.availability !== 'available') throw new Error(`Candidate ABI probe failed: ${abiProbe.reason}`)
|
||||
const request = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'source-candidate-request',
|
||||
documentId: 'source-candidate-document',
|
||||
documentVersion: 1,
|
||||
operationId: 'source-candidate-cut',
|
||||
operation: 'cut',
|
||||
stageId: 'source-candidate:stage:0',
|
||||
resultObjectId: 'source-candidate:result',
|
||||
resultObjectTag: 99,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'source-candidate:object', role: 'object', step: 'ISO-10303-21; probe object', objectTag: 42 },
|
||||
{ inputId: 'tool', objectId: 'source-candidate:tool', role: 'tool', step: 'ISO-10303-21; probe tool', objectTag: 43 },
|
||||
],
|
||||
stages: [{ stageId: 'source-candidate:stage:0', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; probe result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
resultStep: 'ISO-10303-21; probe result',
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 2 }],
|
||||
},
|
||||
})
|
||||
const evidence = captureFreeCadPrivateNamingEvidence(candidate, request, abiProbe)
|
||||
if (!evidence || evidence.status !== 'native-evidence') throw new Error('Candidate ABI returned no native evidence.')
|
||||
if (evidence.mappedNames?.[0]?.reference.name !== '#1' || evidence.mappedNames[0].reference.postfix !== ';:H2a,F') throw new Error('Candidate ABI mapped-name reference is unexpected.')
|
||||
if (evidence.stringHasher?.entries.length !== 1 || evidence.elementMap2?.maps.length !== 1) throw new Error('Candidate ABI native resource evidence is incomplete.')
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: 'freecad-private-naming-abi-candidate-pass',
|
||||
descriptor: abiProbe.descriptor,
|
||||
evidence: {
|
||||
stageId: evidence.stageId,
|
||||
resultObjectId: evidence.resultObjectId,
|
||||
mappedNames: evidence.mappedNames.length,
|
||||
stringHasherEntries: evidence.stringHasher.entries.length,
|
||||
elementMaps: evidence.elementMap2.maps.length,
|
||||
},
|
||||
productionCallbacksExported: false,
|
||||
productionWorkerLinked: false,
|
||||
}, null, 2))
|
||||
@@ -1,10 +1,21 @@
|
||||
#include <emscripten/bind.h>
|
||||
|
||||
#include <App/IndexedName.h>
|
||||
#include <App/ElementMap.h>
|
||||
#include <App/ElementNamingUtils.h>
|
||||
#include <App/MappedName.h>
|
||||
#include <App/MappedElement.h>
|
||||
#include <App/StringHasher.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -26,9 +37,248 @@ std::string freecadPrivateNamingSourceProbe()
|
||||
}
|
||||
return name.toString();
|
||||
}
|
||||
|
||||
std::string freecadPrivateStringHasherSourceProbe()
|
||||
{
|
||||
App::StringHasher hasher;
|
||||
hasher.setSaveAll(true);
|
||||
|
||||
const App::StringIDRef plain = hasher.getID("stable-name");
|
||||
const App::StringIDRef duplicate = hasher.getID("stable-name");
|
||||
if (!plain || plain.value() != duplicate.value() || plain.dataToText() != "stable-name") {
|
||||
throw std::runtime_error("FreeCAD StringHasher source deduplication probe failed");
|
||||
}
|
||||
|
||||
hasher.setThreshold(8);
|
||||
const App::StringIDRef hashed = hasher.getID("hashable-source-value", -1, true);
|
||||
if (!hashed.isHashed() || hashed.deref().data().size() != 20) {
|
||||
throw std::runtime_error("FreeCAD StringHasher source SHA-1 threshold probe failed");
|
||||
}
|
||||
|
||||
Data::MappedName mapped(Data::IndexedName("Edge12"));
|
||||
mapped += ";:M;CUT;:H2a:7,E";
|
||||
const App::StringIDRef mappedID = hasher.getID(mapped, {});
|
||||
if (!mappedID || mappedID.getIndex() != 12 || mappedID.relatedIDs().size() != 2) {
|
||||
throw std::runtime_error("FreeCAD StringHasher mapped-name source probe failed");
|
||||
}
|
||||
|
||||
return "plain=" + std::to_string(plain.value()) + ";hashed="
|
||||
+ std::to_string(hashed.value()) + ";mapped=" + std::to_string(mappedID.value())
|
||||
+ ";index=" + std::to_string(mappedID.getIndex()) + ";related="
|
||||
+ std::to_string(mappedID.relatedIDs().size());
|
||||
}
|
||||
|
||||
std::string freecadPrivateElementMapSourceProbe()
|
||||
{
|
||||
App::StringHasherRef hasher(new App::StringHasher());
|
||||
hasher->setSaveAll(true);
|
||||
|
||||
auto elementMap = std::make_shared<Data::ElementMap>();
|
||||
elementMap->hasher = hasher;
|
||||
Data::MappedName encoded("Edge12;:M;CUT");
|
||||
Data::ElementIDRefs stringIDs;
|
||||
std::ostringstream postfix;
|
||||
elementMap->encodeElementName('F', encoded, postfix, &stringIDs, 99, nullptr, 42, true);
|
||||
if (!encoded.startsWith("#") || encoded.find(";:H2a,F") < 0 || stringIDs.size() != 1) {
|
||||
throw std::runtime_error("FreeCAD ElementMap StringHasher encoding probe failed");
|
||||
}
|
||||
|
||||
const Data::IndexedName face("Face3");
|
||||
const Data::MappedName stored = elementMap->setElementName(face, encoded, 99, &stringIDs);
|
||||
if (!stored || elementMap->find(stored) != face || elementMap->find(face) != stored) {
|
||||
throw std::runtime_error("FreeCAD ElementMap bidirectional lookup probe failed");
|
||||
}
|
||||
|
||||
Data::MappedName original;
|
||||
std::vector<Data::MappedName> history;
|
||||
const long historyTag = elementMap->getElementHistory(stored, 99, &original, &history);
|
||||
if (historyTag != 42 || original.toString() != "Edge12;:M;CUT") {
|
||||
throw std::runtime_error("FreeCAD ElementMap history probe failed");
|
||||
}
|
||||
|
||||
elementMap->beforeSave(hasher);
|
||||
std::ostringstream saved;
|
||||
elementMap->save(saved);
|
||||
auto restored = std::make_shared<Data::ElementMap>();
|
||||
std::istringstream input(saved.str());
|
||||
restored = restored->restore(hasher, input);
|
||||
if (!restored || restored->find(face) != stored || restored->find(stored) != face) {
|
||||
throw std::runtime_error("FreeCAD ElementMap save/restore probe failed");
|
||||
}
|
||||
|
||||
std::vector<Data::MappedName> ordered {
|
||||
Data::MappedName("#b"),
|
||||
Data::MappedName("Edge10"),
|
||||
Data::MappedName("#a"),
|
||||
Data::MappedName("Edge2"),
|
||||
};
|
||||
std::sort(ordered.begin(), ordered.end(), Data::ElementNameComparator {});
|
||||
if (ordered[0].toString() != "Edge2" || ordered[1].toString() != "Edge10"
|
||||
|| ordered[2].toString() != "#a" || ordered[3].toString() != "#b") {
|
||||
throw std::runtime_error("FreeCAD MappedElement stable ordering probe failed");
|
||||
}
|
||||
|
||||
App::DocumentObject object(77);
|
||||
const Data::HistoryItem item(&object, stored);
|
||||
if (item.tag != 77 || Data::oldElementName("Body.;mapped.Face3") != "Body.Face3") {
|
||||
throw std::runtime_error("FreeCAD mapped element host-boundary probe failed");
|
||||
}
|
||||
|
||||
return "stored=" + stored.toString() + ";tag=" + std::to_string(historyTag)
|
||||
+ ";original=" + original.toString() + ";serialized="
|
||||
+ std::to_string(saved.str().size()) + ";restored=" + std::to_string(restored->size());
|
||||
}
|
||||
|
||||
std::string freecadPrivateElementMapResourcesProbe()
|
||||
{
|
||||
App::StringHasherRef hasher(new App::StringHasher());
|
||||
hasher->setSaveAll(true);
|
||||
auto elementMap = std::make_shared<Data::ElementMap>();
|
||||
elementMap->hasher = hasher;
|
||||
|
||||
Data::MappedName encoded("Edge12;:M;CUT");
|
||||
Data::ElementIDRefs stringIDs;
|
||||
std::ostringstream postfix;
|
||||
elementMap->encodeElementName('F', encoded, postfix, &stringIDs, 99, nullptr, 42, true);
|
||||
elementMap->setElementName(Data::IndexedName("Face3"), encoded, 99, &stringIDs);
|
||||
elementMap->beforeSave(hasher);
|
||||
|
||||
std::ostringstream elementMapStream;
|
||||
elementMap->save(elementMapStream);
|
||||
QJsonArray referenceStringIDs;
|
||||
for (const auto& stringID : stringIDs) {
|
||||
referenceStringIDs.append(static_cast<qint64>(stringID.value()));
|
||||
}
|
||||
const App::StringID::IndexID prefixID = App::StringID::fromString(encoded.dataBytes());
|
||||
if (!prefixID || prefixID.index != 0 || referenceStringIDs.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD ElementMap MappedNameRef probe failed");
|
||||
}
|
||||
const QJsonObject mappedNameReference {
|
||||
{"name", QString::fromUtf8(encoded.dataBytes())},
|
||||
{"postfix", QString::fromUtf8(encoded.postfixBytes())},
|
||||
{"prefixStringId", static_cast<qint64>(prefixID.id)},
|
||||
{"stringIds", referenceStringIDs},
|
||||
};
|
||||
QJsonArray entries;
|
||||
for (const auto& [id, reference] : hasher->getIDMap()) {
|
||||
const App::StringID& stringID = reference.deref();
|
||||
int flags = 0;
|
||||
flags |= stringID.isBinary() ? 1 << 0 : 0;
|
||||
flags |= stringID.isHashed() ? 1 << 1 : 0;
|
||||
flags |= stringID.isPostfixEncoded() ? 1 << 2 : 0;
|
||||
flags |= stringID.isPostfixed() ? 1 << 3 : 0;
|
||||
flags |= stringID.isIndexed() ? 1 << 4 : 0;
|
||||
flags |= stringID.isPrefixID() ? 1 << 5 : 0;
|
||||
flags |= stringID.isPrefixIDIndex() ? 1 << 6 : 0;
|
||||
flags |= stringID.isPersistent() ? 1 << 7 : 0;
|
||||
QJsonArray relatedIDs;
|
||||
for (const auto& related : reference.relatedIDs()) {
|
||||
relatedIDs.append(static_cast<qint64>(related.value()));
|
||||
}
|
||||
entries.append(QJsonObject {
|
||||
{"id", static_cast<qint64>(id)},
|
||||
{"flags", flags},
|
||||
{"relatedIds", relatedIDs},
|
||||
{"data", QString::fromUtf8(stringID.data())},
|
||||
{"postfix", QString::fromUtf8(stringID.postfix())},
|
||||
});
|
||||
}
|
||||
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"elementMapText", QString::fromStdString("BeginElementMap v1\n" + elementMapStream.str())},
|
||||
{"mappedNameReference", mappedNameReference},
|
||||
{"stringHasher", QJsonObject {
|
||||
{"schemaVersion", 2},
|
||||
{"nativeVersion", 1},
|
||||
{"entries", entries},
|
||||
}},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
|
||||
int freecadNamingCandidateAbiVersion()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string freecadNamingCandidateCapabilitiesJson()
|
||||
{
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", 1},
|
||||
{"freecadVersion", "1.1.1"},
|
||||
{"sourceCommit", "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"},
|
||||
{"mappedNameRef", true},
|
||||
{"stringHasher", true},
|
||||
{"elementMap2", true},
|
||||
{"operations", QJsonArray {"cut"}},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
|
||||
std::string freecadNamingCandidateEvidenceJson(const std::string& requestJson)
|
||||
{
|
||||
QJsonParseError requestError;
|
||||
const QJsonDocument requestDocument =
|
||||
QJsonDocument::fromJson(QByteArray::fromStdString(requestJson), &requestError);
|
||||
const QJsonObject request = requestDocument.object();
|
||||
if (requestError.error != QJsonParseError::NoError || request.value("schemaVersion").toInt() != 1
|
||||
|| request.value("operation").toString() != "cut"
|
||||
|| request.value("stageId").toString().isEmpty()
|
||||
|| request.value("resultObjectId").toString().isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming candidate request is invalid");
|
||||
}
|
||||
|
||||
QJsonParseError resourcesError;
|
||||
const QJsonDocument resourcesDocument = QJsonDocument::fromJson(
|
||||
QByteArray::fromStdString(freecadPrivateElementMapResourcesProbe()), &resourcesError);
|
||||
if (resourcesError.error != QJsonParseError::NoError) {
|
||||
throw std::runtime_error("FreeCAD naming candidate resources are invalid");
|
||||
}
|
||||
const QJsonObject resources = resourcesDocument.object();
|
||||
QString sourceObjectId = "probe-source";
|
||||
const QJsonArray inputs = request.value("inputs").toArray();
|
||||
if (!inputs.isEmpty() && !inputs[0].toObject().value("objectId").toString().isEmpty()) {
|
||||
sourceObjectId = inputs[0].toObject().value("objectId").toString();
|
||||
}
|
||||
|
||||
const QJsonObject reference = resources.value("mappedNameReference").toObject();
|
||||
if (reference.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming candidate omitted its native MappedNameRef");
|
||||
}
|
||||
const QJsonObject mappedName {
|
||||
{"kind", "face"},
|
||||
{"resultIndex", 2},
|
||||
{"resultPersistentId", "probe-face-3"},
|
||||
{"relation", "modified"},
|
||||
{"reference", reference},
|
||||
{"sourceRefs", QJsonArray {QJsonObject {
|
||||
{"objectId", sourceObjectId},
|
||||
{"persistentId", "Edge12"},
|
||||
}}},
|
||||
};
|
||||
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", 1},
|
||||
{"stageId", request.value("stageId")},
|
||||
{"resultObjectId", request.value("resultObjectId")},
|
||||
{"status", "native-evidence"},
|
||||
{"mappedNames", QJsonArray {mappedName}},
|
||||
{"stringHasher", resources.value("stringHasher")},
|
||||
{"elementMap2Text", resources.value("elementMapText")},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
EMSCRIPTEN_BINDINGS(freecad_private_naming_source_probe)
|
||||
{
|
||||
emscripten::function("freecadPrivateNamingSourceProbe", &freecadPrivateNamingSourceProbe);
|
||||
emscripten::function("freecadPrivateStringHasherSourceProbe",
|
||||
&freecadPrivateStringHasherSourceProbe);
|
||||
emscripten::function("freecadPrivateElementMapSourceProbe",
|
||||
&freecadPrivateElementMapSourceProbe);
|
||||
emscripten::function("freecadPrivateElementMapResourcesProbe",
|
||||
&freecadPrivateElementMapResourcesProbe);
|
||||
emscripten::function("freecadNamingCandidateAbiVersion", &freecadNamingCandidateAbiVersion);
|
||||
emscripten::function("freecadNamingCandidateCapabilitiesJson",
|
||||
&freecadNamingCandidateCapabilitiesJson);
|
||||
emscripten::function("freecadNamingCandidateEvidenceJson",
|
||||
&freecadNamingCandidateEvidenceJson);
|
||||
}
|
||||
|
||||
41
native/freecad-naming-probe/shims/App/Application.h
Normal file
41
native/freecad-naming-probe/shims/App/Application.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef SRC_APP_APPLICATION_H_
|
||||
#define SRC_APP_APPLICATION_H_
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace App {
|
||||
|
||||
class Document;
|
||||
|
||||
class ProbeSignal
|
||||
{
|
||||
public:
|
||||
template<typename Callback>
|
||||
void connect(Callback&&)
|
||||
{}
|
||||
};
|
||||
|
||||
class Application
|
||||
{
|
||||
public:
|
||||
Document* getActiveDocument() const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ProbeSignal signalStartSaveDocument;
|
||||
ProbeSignal signalFinishSaveDocument;
|
||||
ProbeSignal signalStartRestoreDocument;
|
||||
ProbeSignal signalFinishRestoreDocument;
|
||||
};
|
||||
|
||||
inline Application& GetApplication()
|
||||
{
|
||||
static Application application;
|
||||
return application;
|
||||
}
|
||||
|
||||
} // namespace App
|
||||
|
||||
#endif
|
||||
19
native/freecad-naming-probe/shims/App/Document.h
Normal file
19
native/freecad-naming-probe/shims/App/Document.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef SRC_APP_DOCUMENT_H_
|
||||
#define SRC_APP_DOCUMENT_H_
|
||||
|
||||
#include <App/DocumentObject.h>
|
||||
|
||||
namespace App {
|
||||
|
||||
class Document
|
||||
{
|
||||
public:
|
||||
DocumentObject* getObjectByID(long) const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace App
|
||||
|
||||
#endif
|
||||
31
native/freecad-naming-probe/shims/App/DocumentObject.h
Normal file
31
native/freecad-naming-probe/shims/App/DocumentObject.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#ifndef SRC_APP_DOCUMENTOBJECT_H_
|
||||
#define SRC_APP_DOCUMENTOBJECT_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace App {
|
||||
|
||||
class DocumentObject
|
||||
{
|
||||
public:
|
||||
explicit DocumentObject(long id = 0)
|
||||
: objectId(id)
|
||||
{}
|
||||
|
||||
long getID() const
|
||||
{
|
||||
return objectId;
|
||||
}
|
||||
|
||||
std::string getFullName() const
|
||||
{
|
||||
return "ProbeObject";
|
||||
}
|
||||
|
||||
private:
|
||||
long objectId;
|
||||
};
|
||||
|
||||
} // namespace App
|
||||
|
||||
#endif
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define APP_STRING_ID_H
|
||||
|
||||
#include <QByteArray>
|
||||
|
||||
namespace App
|
||||
{
|
||||
class StringIDRef
|
||||
{
|
||||
public:
|
||||
void toBytes(QByteArray& bytes) const
|
||||
{
|
||||
bytes.clear();
|
||||
}
|
||||
|
||||
bool operator<(const StringIDRef&) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator==(const StringIDRef&) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} // namespace App
|
||||
@@ -1,3 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <Base/Exception.h>
|
||||
|
||||
struct FreeCadNamingProbeLogInstance
|
||||
{
|
||||
bool isEnabled(int) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
inline FreeCadNamingProbeLogInstance freecad_naming_probe_log_instance;
|
||||
|
||||
#define FC_LOGLEVEL_LOG 3
|
||||
#define FC_LOGLEVEL_TRACE 4
|
||||
#define FC_LOG_INSTANCE freecad_naming_probe_log_instance
|
||||
#define FC_LOG_LEVEL_INIT(...)
|
||||
#define FC_WARN(message) ((void)0)
|
||||
#define FC_ERR(message) ((void)0)
|
||||
#define FC_LOG(message) ((void)0)
|
||||
#define FC_TRACE(message) ((void)0)
|
||||
|
||||
35
native/freecad-naming-probe/shims/Base/Exception.h
Normal file
35
native/freecad-naming-probe/shims/Base/Exception.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class Exception : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
|
||||
void reportException() const {}
|
||||
};
|
||||
|
||||
class RuntimeError : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
class ValueError : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
|
||||
#define FC_THROWM(type, message) \
|
||||
do { \
|
||||
std::ostringstream freecad_naming_probe_exception_stream; \
|
||||
freecad_naming_probe_exception_stream << message; \
|
||||
throw type(freecad_naming_probe_exception_stream.str()); \
|
||||
} while (false)
|
||||
38
native/freecad-naming-probe/shims/Base/Persistence.h
Normal file
38
native/freecad-naming-probe/shims/Base/Persistence.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <CXX/Objects.hxx>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class Reader;
|
||||
class Writer;
|
||||
class XMLReader;
|
||||
|
||||
class BaseClass
|
||||
{
|
||||
public:
|
||||
virtual ~BaseClass() = default;
|
||||
virtual PyObject* getPyObject()
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
class Persistence : public BaseClass
|
||||
{
|
||||
public:
|
||||
~Persistence() override = default;
|
||||
|
||||
virtual unsigned int getMemSize() const = 0;
|
||||
virtual void Save(Writer&) const = 0;
|
||||
virtual void Restore(XMLReader&) = 0;
|
||||
virtual void SaveDocFile(Writer&) const = 0;
|
||||
virtual void RestoreDocFile(Reader&) = 0;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
|
||||
#define TYPESYSTEM_HEADER()
|
||||
#define TYPESYSTEM_HEADER_WITH_OVERRIDE()
|
||||
#define TYPESYSTEM_SOURCE(...)
|
||||
#define TYPESYSTEM_SOURCE_ABSTRACT(...)
|
||||
3
native/freecad-naming-probe/shims/Base/PyObjectBase.h
Normal file
3
native/freecad-naming-probe/shims/Base/PyObjectBase.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <CXX/Objects.hxx>
|
||||
45
native/freecad-naming-probe/shims/Base/Reader.h
Normal file
45
native/freecad-naming-probe/shims/Base/Reader.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <istream>
|
||||
#include <sstream>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class Reader : public std::istream
|
||||
{
|
||||
public:
|
||||
Reader()
|
||||
: std::istream(nullptr)
|
||||
{}
|
||||
};
|
||||
|
||||
class XMLReader
|
||||
{
|
||||
public:
|
||||
template<typename T>
|
||||
T getAttribute(const char*) const
|
||||
{
|
||||
return T {};
|
||||
}
|
||||
|
||||
bool hasAttribute(const char*) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void readElement(const char*) {}
|
||||
void readEndElement(const char*) {}
|
||||
void addFile(const char*, void*) {}
|
||||
|
||||
std::istream& beginCharStream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
int FileVersion = 1;
|
||||
|
||||
private:
|
||||
std::istringstream stream;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
43
native/freecad-naming-probe/shims/Base/Stream.h
Normal file
43
native/freecad-naming-probe/shims/Base/Stream.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <istream>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class TextOutputStream
|
||||
{
|
||||
public:
|
||||
explicit TextOutputStream(std::ostream& stream)
|
||||
: output(stream)
|
||||
{}
|
||||
|
||||
TextOutputStream& operator<<(const char* value)
|
||||
{
|
||||
output << value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
std::ostream& output;
|
||||
};
|
||||
|
||||
class TextInputStream
|
||||
{
|
||||
public:
|
||||
explicit TextInputStream(std::istream& stream)
|
||||
: input(stream)
|
||||
{}
|
||||
|
||||
TextInputStream& operator>>(std::string& value)
|
||||
{
|
||||
input >> value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
std::istream& input;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
40
native/freecad-naming-probe/shims/Base/Writer.h
Normal file
40
native/freecad-naming-probe/shims/Base/Writer.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class Writer
|
||||
{
|
||||
public:
|
||||
std::ostream& Stream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
const char* ind() const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
const char* addFile(const char* name, const void*)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
std::ostream& beginCharStream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
std::ostream& endCharStream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
private:
|
||||
std::ostringstream stream;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
9
native/freecad-naming-probe/shims/CXX/Objects.hxx
Normal file
9
native/freecad-naming-probe/shims/CXX/Objects.hxx
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
struct _object {};
|
||||
using PyObject = _object;
|
||||
|
||||
inline _object freecad_naming_probe_py_none;
|
||||
|
||||
#define Py_None (&freecad_naming_probe_py_none)
|
||||
#define Py_INCREF(object) ((void)(object))
|
||||
11
native/freecad-naming-probe/shims/FCConfig.h
Normal file
11
native/freecad-naming-probe/shims/FCConfig.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
|
||||
#ifndef AppExport
|
||||
#define AppExport
|
||||
#endif
|
||||
|
||||
#ifndef BaseExport
|
||||
#define BaseExport
|
||||
#endif
|
||||
5
native/freecad-naming-probe/shims/ProbeAppHost.h
Normal file
5
native/freecad-naming-probe/shims/ProbeAppHost.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <App/DocumentObject.h>
|
||||
#include <App/Document.h>
|
||||
#include <App/Application.h>
|
||||
15
native/freecad-naming-probe/shims/StringHasherPy.h
Normal file
15
native/freecad-naming-probe/shims/StringHasherPy.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <CXX/Objects.hxx>
|
||||
|
||||
namespace App {
|
||||
|
||||
class StringHasher;
|
||||
|
||||
class StringHasherPy : public _object
|
||||
{
|
||||
public:
|
||||
explicit StringHasherPy(StringHasher*) {}
|
||||
};
|
||||
|
||||
} // namespace App
|
||||
17
native/freecad-naming-probe/shims/StringIDPy.h
Normal file
17
native/freecad-naming-probe/shims/StringIDPy.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <CXX/Objects.hxx>
|
||||
|
||||
namespace App {
|
||||
|
||||
class StringID;
|
||||
|
||||
class StringIDPy : public _object
|
||||
{
|
||||
public:
|
||||
explicit StringIDPy(StringID*) {}
|
||||
|
||||
int _index = 0;
|
||||
};
|
||||
|
||||
} // namespace App
|
||||
@@ -1,8 +1,22 @@
|
||||
import createProbe from './dist/freecad-private-naming-source-probe.js'
|
||||
import { inspectWasmStaticArchive } from '../../scripts/freecad-naming-sdk-lib.mjs'
|
||||
|
||||
const archive = await inspectWasmStaticArchive(new URL('./dist/libFreeCADPrivateNamingProbe.a', import.meta.url))
|
||||
if (archive.memberCount !== 7 || archive.wasmObjectCount !== 7 || archive.llvmBitcodeCount !== 0) {
|
||||
throw new Error(`Unexpected FreeCAD private naming source archive: ${JSON.stringify(archive)}`)
|
||||
}
|
||||
|
||||
const probe = await createProbe()
|
||||
const result = probe.freecadPrivateNamingSourceProbe()
|
||||
if (result !== 'Edge12;:M;CUT;:H2a:7,E') throw new Error(`Unexpected FreeCAD private naming source probe result: ${result}`)
|
||||
const stringHasherResult = probe.freecadPrivateStringHasherSourceProbe()
|
||||
if (stringHasherResult !== 'plain=1;hashed=2;mapped=5;index=12;related=2') {
|
||||
throw new Error(`Unexpected FreeCAD StringHasher source probe result: ${stringHasherResult}`)
|
||||
}
|
||||
const elementMapResult = probe.freecadPrivateElementMapSourceProbe()
|
||||
if (elementMapResult !== 'stored=#1;:H2a,F;tag=42;original=Edge12;:M;CUT;serialized=115;restored=1') {
|
||||
throw new Error(`Unexpected FreeCAD ElementMap source probe result: ${elementMapResult}`)
|
||||
}
|
||||
for (const forbidden of ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']) {
|
||||
if (typeof probe[forbidden] === 'function') throw new Error(`Source prerequisite probe must not export production ABI callback ${forbidden}.`)
|
||||
}
|
||||
@@ -10,7 +24,22 @@ console.log(JSON.stringify({
|
||||
status: 'source-prerequisite-pass',
|
||||
freecadVersion: '1.1.1',
|
||||
sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d',
|
||||
linkedSources: ['App/IndexedName.cpp', 'App/MappedName.cpp'],
|
||||
linkedSources: [
|
||||
'App/IndexedName.cpp',
|
||||
'App/MappedName.cpp',
|
||||
'App/StringHasher.cpp',
|
||||
'App/MappedElement.cpp',
|
||||
'App/ElementNamingUtils.cpp',
|
||||
'App/ElementMap.cpp',
|
||||
'Base/Handle.cpp',
|
||||
],
|
||||
sourceArchive: {
|
||||
name: 'libFreeCADPrivateNamingProbe.a',
|
||||
...archive,
|
||||
productionEligible: false,
|
||||
},
|
||||
result,
|
||||
stringHasherResult,
|
||||
elementMapResult,
|
||||
productionWorkerLinked: false,
|
||||
}, null, 2))
|
||||
|
||||
@@ -44,19 +44,48 @@ establishes ABI linkage; exact promotion additionally requires valid
|
||||
MappedNameRef, StringHasher, ElementMap2, stage history, and round-trip evidence.
|
||||
|
||||
The repository also carries a prerequisite-only source probe. It builds QtBase
|
||||
6.8.2 `Qt6Core` for wasm, then compiles the locked FreeCAD `IndexedName.cpp` and
|
||||
`MappedName.cpp` sources without exporting the production naming callbacks:
|
||||
6.8.2 `Qt6Core` for wasm, then compiles the locked FreeCAD `IndexedName.cpp`,
|
||||
`MappedName.cpp`, `StringHasher.cpp`, `MappedElement.cpp`,
|
||||
`ElementNamingUtils.cpp`, `ElementMap.cpp`, and `Base/Handle.cpp` sources without
|
||||
exporting the production naming callbacks:
|
||||
|
||||
```bash
|
||||
./npmw run build:qt6-wasm-core
|
||||
./npmw run build:freecad-naming-source-probe
|
||||
./npmw run test:freecad-naming-source-probe
|
||||
./npmw run check:freecad-naming-sdk-readiness
|
||||
./npmw run check:freecad-private-naming-boundary
|
||||
```
|
||||
|
||||
Passing this probe establishes that the private source subset and toolchain are
|
||||
cross-compilable. It does not link the production OCCT Worker, does not close
|
||||
EX-TSN-02, and does not change `systemExact=false`.
|
||||
cross-compilable. Its runtime test covers mapped-name parsing, StringHasher
|
||||
deduplication, SHA-1 threshold handling, indexed mapped-name references,
|
||||
ElementMap encoding/lookup/history/save/restore, and stable mapped-element
|
||||
ordering. It also feeds native MappedNameRef, StringHasher, and ElementMap2
|
||||
resources through the strict Web ABI validator using deliberately separate
|
||||
`freecadNamingCandidate*` callback names.
|
||||
|
||||
The seven locked FreeCAD source units are first compiled into the isolated
|
||||
`libFreeCADPrivateNamingProbe.a` archive. The smoke test checks that it contains
|
||||
exactly seven wasm object members before linking the runnable probe. This
|
||||
archive remains host-adapter-bound and is not a substitute for FreeCADBase or
|
||||
FreeCADApp.
|
||||
|
||||
Persistence, Python wrappers, type-system, logging, and Application/Document
|
||||
lifecycle interfaces remain standalone host adapters. FreeCADApp/Part/Python
|
||||
static libraries, real Application/Document integration, OCCT builder context,
|
||||
and the production bridge are not linked. The candidate callbacks are never
|
||||
published to the production Worker. The probe therefore does not close
|
||||
EX-TSN-02 and does not change `systemExact=false`.
|
||||
|
||||
`check:freecad-naming-sdk-readiness` audits the pre-production SDK plan without
|
||||
publishing anything. It parses every present `.a` archive and rejects native
|
||||
ELF members; the current local resource set verifies QtCore plus its bundled
|
||||
Pcre2/Zlib wasm dependencies, but still lacks FreeCADBase, FreeCADApp, Part,
|
||||
Python, and the production bridge. `generate:freecad-naming-sdk-manifest` is
|
||||
fail-closed and writes no manifest until all inputs are present. The complete
|
||||
SDK checker applies the same wasm-archive rule, so an x86 static library cannot
|
||||
satisfy the manifest by hash alone.
|
||||
|
||||
The versioned JSON request carries document `objectId`, stable positive
|
||||
`objectTag`, prior naming evidence, result object identity, result tag, stage
|
||||
|
||||
@@ -20,6 +20,7 @@ if [[ -z "${OCCT_BUILD_DIR:-}" ]]; then
|
||||
fi
|
||||
DIST_DIR="${OCCT_HISTORY_DIST_DIR:-${ROOT_DIR}/native/occt-history/dist}"
|
||||
PUBLIC_DIR="${OCCT_HISTORY_PUBLIC_DIR:-${ROOT_DIR}/public/native/occt-history}"
|
||||
PUBLISH_ARTIFACT="${OCCT_HISTORY_PUBLISH:-1}"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
FREECAD_WASM_SDK_DIR="${FREECAD_WASM_SDK_DIR:-}"
|
||||
|
||||
@@ -40,11 +41,18 @@ if ! command -v emcmake >/dev/null || ! command -v em++ >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OCCT_CXX_FLAGS="-fexceptions"
|
||||
OCCT_CXX_RELEASE_FLAGS="-O0 -DNDEBUG -fexceptions"
|
||||
if [[ -n "${FREECAD_WASM_SDK_DIR}" ]]; then
|
||||
OCCT_CXX_FLAGS+=" -pthread"
|
||||
OCCT_CXX_RELEASE_FLAGS+=" -pthread"
|
||||
fi
|
||||
|
||||
emcmake cmake -S "${OCCT_SOURCE_DIR}" -B "${OCCT_BUILD_DIR}" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_MAKE_PROGRAM="$(command -v ninja)" \
|
||||
-DCMAKE_CXX_FLAGS="-fexceptions" \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O0 -DNDEBUG -fexceptions" \
|
||||
-DCMAKE_CXX_FLAGS="${OCCT_CXX_FLAGS}" \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="${OCCT_CXX_RELEASE_FLAGS}" \
|
||||
-DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF \
|
||||
-DBUILD_LIBRARY_TYPE=Static \
|
||||
-DBUILD_MODULE_FoundationClasses=ON \
|
||||
@@ -74,18 +82,29 @@ SOURCES=("${ROOT_DIR}/native/occt-history/occt_history.cpp")
|
||||
INCLUDES=("-I${OCCT_BUILD_DIR}/include/opencascade" "-I${OCCT_SOURCE_DIR}/src")
|
||||
DEFINES=()
|
||||
FREECAD_LIBRARIES=()
|
||||
FREECAD_LINK_OPTIONS=()
|
||||
if [[ -n "${FREECAD_WASM_SDK_DIR}" ]]; then
|
||||
BRIDGE_SOURCE="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); console.log(p.isAbsolute(m.namingBridge.source) ? m.namingBridge.source : p.resolve(process.argv[1],m.namingBridge.source))' "${FREECAD_WASM_SDK_DIR}")"
|
||||
SOURCES+=("${BRIDGE_SOURCE}")
|
||||
while IFS= read -r include_dir; do INCLUDES+=("-I${include_dir}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const d of m.includeDirs) console.log(p.isAbsolute(d) ? d : p.resolve(root,d))' "${FREECAD_WASM_SDK_DIR}")
|
||||
while IFS= read -r library; do FREECAD_LIBRARIES+=("${library}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const l of m.libraries) console.log(p.isAbsolute(l.path) ? l.path : p.resolve(root,l.path))' "${FREECAD_WASM_SDK_DIR}")
|
||||
while IFS= read -r definition; do DEFINES+=("-D${definition}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); for (const d of m.compileOptions.definitions) console.log(d)' "${FREECAD_WASM_SDK_DIR}")
|
||||
CXX_STANDARD="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); console.log(m.compileOptions.cxxStandard)' "${FREECAD_WASM_SDK_DIR}")"
|
||||
FORCE_INCLUDE="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); const v=m.compileOptions.forceInclude; console.log(p.isAbsolute(v) ? v : p.resolve(root,v))' "${FREECAD_WASM_SDK_DIR}")"
|
||||
HOST_ADAPTER="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); const v=m.namingBridge.hostAdapter; console.log(p.isAbsolute(v) ? v : p.resolve(root,v))' "${FREECAD_WASM_SDK_DIR}")"
|
||||
FREECAD_LINK_OPTIONS+=("-include" "${FORCE_INCLUDE}" "-pthread" "--pre-js" "${HOST_ADAPTER}" "-sPTHREAD_POOL_SIZE=2")
|
||||
while IFS=$'\t' read -r asset_path preload_to; do
|
||||
FREECAD_LINK_OPTIONS+=("--preload-file" "${asset_path}@${preload_to}")
|
||||
done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const a of m.runtimeAssets) { const v=p.isAbsolute(a.path) ? a.path : p.resolve(root,a.path); console.log(`${v}\t${a.preloadTo}`) }' "${FREECAD_WASM_SDK_DIR}")
|
||||
DEFINES+=("-DBITBYBIT_FREECAD_NAMING_LINKED=1")
|
||||
else
|
||||
CXX_STANDARD="c++17"
|
||||
fi
|
||||
|
||||
em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" \
|
||||
em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" "${FREECAD_LINK_OPTIONS[@]}" \
|
||||
-I"${OCCT_BUILD_DIR}/include/opencascade" \
|
||||
-I"${OCCT_SOURCE_DIR}/src" \
|
||||
-std=c++17 -O2 -fexceptions --bind \
|
||||
-std="${CXX_STANDARD}" -O2 -fexceptions --bind \
|
||||
-Wl,--start-group \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKBO.a" \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKFillet.a" \
|
||||
@@ -112,7 +131,13 @@ em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" \
|
||||
-o "${DIST_DIR}/bitbybit-occt-history.js"
|
||||
|
||||
cp "${ROOT_DIR}/native/occt-history/package.json" "${DIST_DIR}/package.json"
|
||||
mkdir -p "${PUBLIC_DIR}"
|
||||
cp "${DIST_DIR}/bitbybit-occt-history.js" "${PUBLIC_DIR}/bitbybit-occt-history.js"
|
||||
cp "${DIST_DIR}/bitbybit-occt-history.wasm" "${PUBLIC_DIR}/bitbybit-occt-history.wasm"
|
||||
sha256sum "${DIST_DIR}/bitbybit-occt-history.js" "${DIST_DIR}/bitbybit-occt-history.wasm"
|
||||
if [[ "${PUBLISH_ARTIFACT}" == "1" ]]; then
|
||||
mkdir -p "${PUBLIC_DIR}"
|
||||
for artifact in "${DIST_DIR}"/bitbybit-occt-history.{js,wasm,data,worker.js}; do
|
||||
[[ -f "${artifact}" ]] && cp "${artifact}" "${PUBLIC_DIR}/$(basename "${artifact}")"
|
||||
done
|
||||
elif [[ "${PUBLISH_ARTIFACT}" != "0" ]]; then
|
||||
echo "OCCT_HISTORY_PUBLISH must be 0 or 1." >&2
|
||||
exit 1
|
||||
fi
|
||||
sha256sum "${DIST_DIR}"/bitbybit-occt-history.{js,wasm,data} 2>/dev/null || sha256sum "${DIST_DIR}/bitbybit-occt-history.js" "${DIST_DIR}/bitbybit-occt-history.wasm"
|
||||
|
||||
149
native/occt-history/freecad-naming-candidate-smoke.ts
Normal file
149
native/occt-history/freecad-naming-candidate-smoke.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { dirname } from 'node:path'
|
||||
import { resolve } from 'node:path'
|
||||
import {
|
||||
captureFreeCadPrivateNamingEvidence,
|
||||
createFreeCadPrivateNamingAbiRequest,
|
||||
probeFreeCadPrivateNamingAbi,
|
||||
type NativeFreeCadNamingAbiModule,
|
||||
} from '../../src/facade/nativeNamingAbi'
|
||||
|
||||
type CandidateModule = NativeFreeCadNamingAbiModule & {
|
||||
makeBox(x: number, y: number, z: number): unknown
|
||||
shapeToStep(shape: unknown): string
|
||||
booleanHistoryFromStep(objectStep: string, toolStep: string, operation: 'cut'): {
|
||||
provider: 'occt-native'
|
||||
occtVersion: string
|
||||
hasModified: boolean
|
||||
hasGenerated: boolean
|
||||
hasDeleted: boolean
|
||||
resultStep: string
|
||||
records: Array<{
|
||||
relation: 'modified' | 'generated' | 'deleted'
|
||||
source: string
|
||||
kind: 'face' | 'edge' | 'vertex'
|
||||
resultKind?: 'face' | 'edge' | 'vertex'
|
||||
sourceIndex: number
|
||||
resultIndex?: number
|
||||
resultIndexes?: number[]
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
const dist = resolve(process.env.FREECAD_NAMING_WORKER_DIST ?? '.cache/candidates/freecad-naming-worker')
|
||||
const moduleUrl = pathToFileURL(resolve(dist, 'bitbybit-occt-history.js')).href
|
||||
const createCandidate = (await import(moduleUrl)).default as (options: { locateFile(path: string): string }) => Promise<CandidateModule>
|
||||
const candidate = await createCandidate({ locateFile: (path) => resolve(dist, path) })
|
||||
const probe = probeFreeCadPrivateNamingAbi(candidate)
|
||||
if (probe.availability !== 'available') throw new Error(`Candidate Worker naming ABI probe failed: ${probe.reason}`)
|
||||
|
||||
const object = candidate.makeBox(10, 10, 10)
|
||||
const tool = candidate.makeBox(5, 5, 5)
|
||||
const objectStep = candidate.shapeToStep(object)
|
||||
const toolStep = candidate.shapeToStep(tool)
|
||||
const history = candidate.booleanHistoryFromStep(objectStep, toolStep, 'cut')
|
||||
const record = history.records.find((entry) => entry.relation !== 'deleted'
|
||||
&& Number.isSafeInteger(entry.sourceIndex)
|
||||
&& (Number.isSafeInteger(entry.resultIndex) || entry.resultIndexes?.some(Number.isSafeInteger)))
|
||||
if (!record) throw new Error('Candidate Worker OCCT cut returned no usable native history record.')
|
||||
const resultIndex = Number.isSafeInteger(record.resultIndex) ? record.resultIndex! : record.resultIndexes!.find(Number.isSafeInteger)!
|
||||
const selectedRecord = { ...record, resultIndex, resultIndexes: undefined }
|
||||
const evidence = captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'candidate-worker-cut-1',
|
||||
documentId: 'candidate-worker-document',
|
||||
documentVersion: 1,
|
||||
operationId: 'candidate-worker-cut',
|
||||
operation: 'cut',
|
||||
stageId: 'candidate-worker:stage:1',
|
||||
resultObjectId: 'candidate-worker:result:1',
|
||||
resultObjectTag: 100,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'candidate-object', role: 'object', objectTag: 42, step: objectStep },
|
||||
{ inputId: 'tool', objectId: 'candidate-tool', role: 'tool', objectTag: 43, step: toolStep },
|
||||
],
|
||||
stages: [{ stageId: 'candidate-worker:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: history.resultStep,
|
||||
history: { ...history, records: [selectedRecord] },
|
||||
}), probe)
|
||||
if (!evidence?.elementMap2 || !evidence.stringHasher || evidence.mappedNames?.length !== 1) throw new Error('Candidate Worker returned incomplete FreeCAD naming evidence for OCCT cut history.')
|
||||
const secondEvidence = captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'candidate-worker-cut-2',
|
||||
documentId: 'candidate-worker-document',
|
||||
documentVersion: 2,
|
||||
operationId: 'candidate-worker-cut-2',
|
||||
operation: 'cut',
|
||||
stageId: 'candidate-worker:stage:2',
|
||||
resultObjectId: 'candidate-worker:result:2',
|
||||
resultObjectTag: 101,
|
||||
inputs: [{ inputId: 'object', objectId: 'candidate-worker:result:1', role: 'object', stageId: 'candidate-worker:stage:1', objectTag: 100, step: history.resultStep, namingEvidence: evidence }],
|
||||
stages: [{ stageId: 'candidate-worker:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: history.resultStep,
|
||||
history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind ?? record.kind, sourceIndex: resultIndex, resultIndex: resultIndex + 1 }] },
|
||||
}), probe)
|
||||
const thirdEvidence = secondEvidence && captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'candidate-worker-cut-3',
|
||||
documentId: 'candidate-worker-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'candidate-worker-cut-3',
|
||||
operation: 'cut',
|
||||
stageId: 'candidate-worker:stage:3',
|
||||
resultObjectId: 'candidate-worker:result:3',
|
||||
resultObjectTag: 102,
|
||||
inputs: [{ inputId: 'object', objectId: 'candidate-worker:result:2', role: 'object', stageId: 'candidate-worker:stage:2', objectTag: 101, step: history.resultStep, namingEvidence: secondEvidence }],
|
||||
stages: [{ stageId: 'candidate-worker:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: history.resultStep,
|
||||
history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind ?? record.kind, sourceIndex: resultIndex + 1, resultIndex: resultIndex + 2 }] },
|
||||
}), probe)
|
||||
if (!secondEvidence?.stringHasher || !thirdEvidence?.stringHasher || secondEvidence.stringHasher.entries.length < 1 || thirdEvidence.stringHasher.entries.length <= secondEvidence.stringHasher.entries.length) throw new Error('Candidate Worker did not restore non-empty StringHasher evidence across three stages.')
|
||||
|
||||
let rejected = false
|
||||
try {
|
||||
captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'candidate-worker-invalid',
|
||||
documentId: 'candidate-worker-document',
|
||||
documentVersion: 2,
|
||||
operationId: 'candidate-worker-invalid',
|
||||
operation: 'cut',
|
||||
stageId: 'candidate-worker:invalid',
|
||||
resultObjectId: 'candidate-worker:invalid-result',
|
||||
resultObjectTag: 101,
|
||||
inputs: [{ inputId: 'object', objectId: 'candidate-object', role: 'object', objectTag: 42, step: objectStep }],
|
||||
stages: [{ stageId: 'candidate-worker:invalid', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
history: { ...history, records: [] },
|
||||
}), probe)
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error) || !error.message.includes('requires inputs and native history records')) throw error
|
||||
rejected = true
|
||||
}
|
||||
if (!rejected) throw new Error('Candidate Worker accepted incomplete native history.')
|
||||
|
||||
const result = {
|
||||
status: 'freecad-naming-candidate-worker-pass',
|
||||
occtVersion: history.occtVersion,
|
||||
callbacks: ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'],
|
||||
namingEvidence: {
|
||||
mappedNames: evidence.mappedNames.length,
|
||||
stringHasherEntries: evidence.stringHasher.entries.length,
|
||||
elementMaps: evidence.elementMap2.maps.length,
|
||||
},
|
||||
chainedStage: { mappedNames: secondEvidence.mappedNames.length, stringHasherEntries: secondEvidence.stringHasher.entries.length },
|
||||
thirdStage: { mappedNames: thirdEvidence.mappedNames.length, stringHasherEntries: thirdEvidence.stringHasher.entries.length },
|
||||
invalidHistoryRejected: true,
|
||||
candidateOnly: true,
|
||||
productionPublication: false,
|
||||
productionWorkerLinked: false,
|
||||
}
|
||||
const reportPath = process.env.FREECAD_NAMING_WORKER_REPORT
|
||||
if (reportPath) {
|
||||
const artifacts = await Promise.all(['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data'].map(async (name) => {
|
||||
const path = resolve(dist, name)
|
||||
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
|
||||
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
|
||||
}))
|
||||
const absoluteReportPath = resolve(reportPath)
|
||||
await mkdir(dirname(absoluteReportPath), { recursive: true })
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify({ schemaVersion: 1, ...result, artifacts }, null, 2)}\n`)
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
@@ -14,15 +14,60 @@
|
||||
{ "name": "FreeCADApp", "path": "lib/libFreeCADApp.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "Part", "path": "lib/libPart.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtCore", "path": "lib/libQt6Core.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "Python", "path": "lib/libpython3.a", "sha256": "replace-with-64-lowercase-hex-digits" }
|
||||
{ "name": "QtConcurrent", "path": "lib/libQt6Concurrent.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtNetwork", "path": "lib/libQt6Network.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtXml", "path": "lib/libQt6Xml.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtBundledPcre2", "path": "lib/libQt6BundledPcre2.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtBundledZLIB", "path": "lib/libQt6BundledZLIB.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "yaml-cpp", "path": "lib/libyaml-cpp.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "ICUCommon", "path": "lib/libicuuc.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "ICUI18N", "path": "lib/libicui18n.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "ICUData", "path": "lib/libicudata.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "XercesC", "path": "lib/libxerces-c.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostProgramOptions", "path": "lib/libboost_program_options.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostRegex", "path": "lib/libboost_regex.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostThread", "path": "lib/libboost_thread.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostDateTime", "path": "lib/libboost_date_time.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostAtomic", "path": "lib/libboost_atomic.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "Python", "path": "lib/libpython3.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonMpdecimal", "path": "lib/python-deps/libmpdec.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonExpat", "path": "lib/python-deps/libexpat.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonHaclSha2", "path": "lib/python-deps/libHacl_Hash_SHA2.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonZlib", "path": "lib/python-deps/libz.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonBzip2", "path": "lib/python-deps/libbz2.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonSqlite3", "path": "lib/python-deps/libsqlite3-mt.a", "sha256": "replace-with-64-lowercase-hex-digits" }
|
||||
],
|
||||
"namingBridge": {
|
||||
"source": "src/freecad_naming_bridge.cpp",
|
||||
"sha256": "replace-with-64-lowercase-hex-digits",
|
||||
"hostAdapter": "runtime/freecad_naming_pre.js",
|
||||
"hostAdapterSha256": "replace-with-64-lowercase-hex-digits",
|
||||
"exports": [
|
||||
"freecadNamingAbiVersion",
|
||||
"freecadNamingCapabilitiesJson",
|
||||
"freecadNamingEvidenceJson"
|
||||
]
|
||||
},
|
||||
"compileOptions": {
|
||||
"cxxStandard": "c++20",
|
||||
"pthread": true,
|
||||
"forceInclude": "include/freecad-wasm-sdk-compat.h",
|
||||
"forceIncludeSha256": "replace-with-64-lowercase-hex-digits",
|
||||
"definitions": ["__linux__=1", "QT_NO_KEYWORDS", "HAVE_CONFIG_H", "PYCXX_6_2_COMPATIBILITY"]
|
||||
},
|
||||
"runtimeAssets": [{
|
||||
"name": "PythonWasmStdlib",
|
||||
"path": "runtime/python",
|
||||
"preloadTo": "/usr/local",
|
||||
"files": [
|
||||
{ "path": "lib/python313.zip", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "path": "lib/python3.13/os.py", "sha256": "replace-with-64-lowercase-hex-digits" }
|
||||
]
|
||||
}],
|
||||
"productionPublication": false,
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"systemExact": false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user