From ab2b622730bce382a0074ecee72a964250c5cd70 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 27 Jul 2026 20:59:48 +0200 Subject: [PATCH 1/3] feat!: collapse `FileMeta` into a single type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FileMeta` carried an `std::optional`, so every reader of a document property went through two hops and an optional check, and every backend building one had to remember to materialise the nested struct first. The indirection bought nothing: `DocumentMeta` was never handed out independently except through `DocumentFile::document_meta()`, which is just a second way of reading what `file_meta()` already returns. `DocumentMeta`'s fields move up into `FileMeta` and the optional's presence check becomes `document_type != DocumentType::unknown`. `FileMeta` is now a plain aggregate — the backends that spelled `{file_type(), mimetype(), false, std::nullopt}` use designated initializers instead. The JSON from `meta_to_json` is unchanged: it already flattened both structs into one object. BREAKING CHANGE: - `odr::DocumentMeta` is gone; its fields are members of `odr::FileMeta`. - `FileMeta::document_meta` is gone. `document_type` is now a direct member, `unknown` when the file is not a document; the remaining document fields are only meaningful when it is set. - `DocumentFile::document_meta()` and the `abstract::DocumentFile::document_meta()` virtual are gone — use `file_meta()`. - `FileMeta`'s two constructors are gone; it is an aggregate. - Java: `DocumentMeta` is gone, its fields are on `FileMeta`, and `DocumentFile.documentMeta()` is removed. - Python: `DocumentMeta` is gone, its fields are on `FileMeta`, and `DocumentFile.document_meta()` is removed. The PDF backend keeps its all-or-nothing metadata semantics: a malformed structure leaves every document field unset rather than half-filled. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KbnNCsz6rkVKHW2MWL47m5 --- docs/design/README.md | 5 --- jni/CMakeLists.txt | 1 - .../app/opendocument/core/DocumentFile.java | 6 --- .../app/opendocument/core/DocumentMeta.java | 39 ----------------- jni/java/app/opendocument/core/FileMeta.java | 43 +++++++++++++++++-- jni/src/jni_convert.hpp | 1 - jni/src/jni_file.cpp | 9 ---- jni/src/jni_style.cpp | 22 +++------- jni/tests/app/opendocument/core/FileTest.java | 5 +-- python/src/bind_file.cpp | 26 +++++------ python/tests/test_document.py | 2 +- python/tests/test_file.py | 4 ++ src/odr/file.cpp | 18 -------- src/odr/file.hpp | 26 ++++------- src/odr/internal/abstract/file.hpp | 1 - src/odr/internal/cfb/cfb_file.cpp | 3 +- src/odr/internal/csv/csv_file.cpp | 3 +- src/odr/internal/font/font_file.cpp | 3 +- src/odr/internal/json/json_file.cpp | 3 +- src/odr/internal/odf/odf_file.cpp | 10 +---- src/odr/internal/odf/odf_file.hpp | 1 - src/odr/internal/odf/odf_meta.cpp | 16 +++---- src/odr/internal/oldms/oldms_file.cpp | 13 +----- src/odr/internal/oldms/oldms_file.hpp | 1 - src/odr/internal/ooxml/ooxml_file.cpp | 10 +---- src/odr/internal/ooxml/ooxml_file.hpp | 1 - src/odr/internal/ooxml/ooxml_meta.cpp | 3 +- src/odr/internal/pdf/AGENTS.md | 7 +-- src/odr/internal/pdf/pdf_file.cpp | 16 +++---- src/odr/internal/svm/svm_file.cpp | 3 +- src/odr/internal/text/text_file.cpp | 3 +- src/odr/internal/util/odr_meta_util.cpp | 27 ++++++------ src/odr/internal/zip/zip_file.cpp | 3 +- test/src/html_output_test.cpp | 6 +-- test/src/internal/pdf/pdf_file.cpp | 40 ++++++++--------- 35 files changed, 140 insertions(+), 240 deletions(-) delete mode 100644 jni/java/app/opendocument/core/DocumentMeta.java diff --git a/docs/design/README.md b/docs/design/README.md index c58142a2e..d2908ec66 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -34,11 +34,6 @@ ## Future -### Breaking API changes (next major) - -- Collapse `FileMeta` into a single type. The nested `DocumentMeta` - does not earn the extra indirection. - ### Document index - replace iterators where it makes sense with an index diff --git a/jni/CMakeLists.txt b/jni/CMakeLists.txt index b2bb12c8d..9ee37fe3d 100644 --- a/jni/CMakeLists.txt +++ b/jni/CMakeLists.txt @@ -58,7 +58,6 @@ add_jar(odr_java "java/app/opendocument/core/DirectionalString.java" "java/app/opendocument/core/Document.java" "java/app/opendocument/core/DocumentFile.java" - "java/app/opendocument/core/DocumentMeta.java" "java/app/opendocument/core/DocumentPath.java" "java/app/opendocument/core/DocumentType.java" "java/app/opendocument/core/Element.java" diff --git a/jni/java/app/opendocument/core/DocumentFile.java b/jni/java/app/opendocument/core/DocumentFile.java index 80331da20..08cb985d8 100644 --- a/jni/java/app/opendocument/core/DocumentFile.java +++ b/jni/java/app/opendocument/core/DocumentFile.java @@ -22,10 +22,6 @@ public DocumentType documentType() { return DocumentType.fromNative(documentTypeNative(handle())); } - public DocumentMeta documentMeta() { - return documentMetaNative(handle()); - } - /** Returns a decrypted copy of this file. */ @Override public DocumentFile decrypt(String password) { @@ -44,8 +40,6 @@ public Document document() { private native int documentTypeNative(long handle); - private native DocumentMeta documentMetaNative(long handle); - private native long decryptDocumentFileNative(long handle, String password); private native long documentNative(long handle); diff --git a/jni/java/app/opendocument/core/DocumentMeta.java b/jni/java/app/opendocument/core/DocumentMeta.java deleted file mode 100644 index 6963397b4..000000000 --- a/jni/java/app/opendocument/core/DocumentMeta.java +++ /dev/null @@ -1,39 +0,0 @@ -package app.opendocument.core; - -/** Meta information about a document. Mirrors {@code odr::DocumentMeta}. */ -public final class DocumentMeta { - public final DocumentType documentType; - /** Number of entries (pages/slides/sheets); {@code null} if unknown. */ - public final Long entryCount; - public final String title; - public final String author; - public final String subject; - public final String keywords; - public final String creator; - public final String producer; - public final String creationDate; - public final String modificationDate; - - DocumentMeta( - int documentType, - Long entryCount, - String title, - String author, - String subject, - String keywords, - String creator, - String producer, - String creationDate, - String modificationDate) { - this.documentType = DocumentType.fromNative(documentType); - this.entryCount = entryCount; - this.title = title; - this.author = author; - this.subject = subject; - this.keywords = keywords; - this.creator = creator; - this.producer = producer; - this.creationDate = creationDate; - this.modificationDate = modificationDate; - } -} diff --git a/jni/java/app/opendocument/core/FileMeta.java b/jni/java/app/opendocument/core/FileMeta.java index 6b4603838..eb91fac1b 100644 --- a/jni/java/app/opendocument/core/FileMeta.java +++ b/jni/java/app/opendocument/core/FileMeta.java @@ -5,13 +5,48 @@ public final class FileMeta { public final FileType type; public final String mimetype; public final boolean passwordEncrypted; - /** {@code null} when the file is not a document. */ - public final DocumentMeta documentMeta; - FileMeta(int type, String mimetype, boolean passwordEncrypted, DocumentMeta documentMeta) { + /** {@code UNKNOWN} when the file is not a document; the fields below are then unset. */ + public final DocumentType documentType; + + /** Number of entries (pages/slides/sheets); {@code null} if unknown. */ + public final Long entryCount; + + public final String title; + public final String author; + public final String subject; + public final String keywords; + public final String creator; + public final String producer; + public final String creationDate; + public final String modificationDate; + + FileMeta( + int type, + String mimetype, + boolean passwordEncrypted, + int documentType, + Long entryCount, + String title, + String author, + String subject, + String keywords, + String creator, + String producer, + String creationDate, + String modificationDate) { this.type = FileType.fromNative(type); this.mimetype = mimetype; this.passwordEncrypted = passwordEncrypted; - this.documentMeta = documentMeta; + this.documentType = DocumentType.fromNative(documentType); + this.entryCount = entryCount; + this.title = title; + this.author = author; + this.subject = subject; + this.keywords = keywords; + this.creator = creator; + this.producer = producer; + this.creationDate = creationDate; + this.modificationDate = modificationDate; } } diff --git a/jni/src/jni_convert.hpp b/jni/src/jni_convert.hpp index e50797d24..b88379314 100644 --- a/jni/src/jni_convert.hpp +++ b/jni/src/jni_convert.hpp @@ -33,7 +33,6 @@ jobject make_page_layout(JNIEnv *env, const odr::PageLayout &layout); jobject make_table_dimensions(JNIEnv *env, const odr::TableDimensions &dimensions); jobject make_table_position(JNIEnv *env, const odr::TablePosition &position); -jobject make_document_meta(JNIEnv *env, const odr::DocumentMeta &meta); jobject make_file_meta(JNIEnv *env, const odr::FileMeta &meta); jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config); diff --git a/jni/src/jni_file.cpp b/jni/src/jni_file.cpp index 5b956d061..0e7dd0466 100644 --- a/jni/src/jni_file.cpp +++ b/jni/src/jni_file.cpp @@ -356,15 +356,6 @@ Java_app_opendocument_core_DocumentFile_documentTypeNative(JNIEnv *env, jobject, }); } -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_DocumentFile_documentMetaNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_document_meta( - env, decoded(handle).as_document_file().document_meta()); - }); -} - extern "C" JNIEXPORT jlong JNICALL Java_app_opendocument_core_DocumentFile_decryptDocumentFileNative( JNIEnv *env, jobject, jlong handle, jstring password) { diff --git a/jni/src/jni_style.cpp b/jni/src/jni_style.cpp index f8b5072de..ac28fe34d 100644 --- a/jni/src/jni_style.cpp +++ b/jni/src/jni_style.cpp @@ -251,12 +251,15 @@ jobject make_table_position(JNIEnv *env, const odr::TablePosition &position) { static_cast(position.row)); } -jobject make_document_meta(JNIEnv *env, const odr::DocumentMeta &meta) { +jobject make_file_meta(JNIEnv *env, const odr::FileMeta &meta) { return new_object( - env, "app/opendocument/core/DocumentMeta", - "(ILjava/lang/Long;Ljava/lang/String;Ljava/lang/String;" + env, "app/opendocument/core/FileMeta", + "(ILjava/lang/String;ZILjava/lang/Long;Ljava/lang/String;" + "Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;" - "Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + "Ljava/lang/String;)V", + static_cast(meta.type), to_jstring(env, meta.mimetype), + static_cast(meta.password_encrypted), static_cast(meta.document_type), box_long(env, meta.entry_count), make_string_opt(env, meta.title), make_string_opt(env, meta.author), make_string_opt(env, meta.subject), make_string_opt(env, meta.keywords), @@ -265,17 +268,6 @@ jobject make_document_meta(JNIEnv *env, const odr::DocumentMeta &meta) { make_string_opt(env, meta.modification_date)); } -jobject make_file_meta(JNIEnv *env, const odr::FileMeta &meta) { - jobject document_meta = meta.document_meta.has_value() - ? make_document_meta(env, *meta.document_meta) - : nullptr; - return new_object( - env, "app/opendocument/core/FileMeta", - "(ILjava/lang/String;ZLapp/opendocument/core/DocumentMeta;)V", - static_cast(meta.type), to_jstring(env, meta.mimetype), - static_cast(meta.password_encrypted), document_meta); -} - jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config) { jclass cls = env->FindClass("app/opendocument/core/HtmlConfig"); if (cls == nullptr) { diff --git a/jni/tests/app/opendocument/core/FileTest.java b/jni/tests/app/opendocument/core/FileTest.java index 20b447f73..6ed5e756d 100644 --- a/jni/tests/app/opendocument/core/FileTest.java +++ b/jni/tests/app/opendocument/core/FileTest.java @@ -25,8 +25,7 @@ void openOdt() throws IOException { DocumentFile documentFile = file.asDocumentFile(); assertEquals(DocumentType.TEXT, documentFile.documentType()); - DocumentMeta meta = documentFile.documentMeta(); - assertEquals(DocumentType.TEXT, meta.documentType); + assertEquals(DocumentType.TEXT, documentFile.fileMeta().documentType); } } @@ -66,7 +65,7 @@ void fileMeta() throws IOException { FileMeta meta = file.fileMeta(); assertEquals(FileType.OPENDOCUMENT_TEXT, meta.type); assertFalse(meta.passwordEncrypted); - assertNotNull(meta.documentMeta); + assertEquals(DocumentType.TEXT, meta.documentType); } } diff --git a/python/src/bind_file.cpp b/python/src/bind_file.cpp index a1f57dc2c..b5f6380c6 100644 --- a/python/src/bind_file.cpp +++ b/python/src/bind_file.cpp @@ -93,20 +93,6 @@ void odr_python::bind_file(py::module_ &m) { .def_readwrite("engine_priority", &odr::DecodePreference::engine_priority); - py::class_(m, "DocumentMeta") - .def(py::init<>()) - .def_readwrite("document_type", &odr::DocumentMeta::document_type) - .def_readwrite("entry_count", &odr::DocumentMeta::entry_count) - .def_readwrite("title", &odr::DocumentMeta::title) - .def_readwrite("author", &odr::DocumentMeta::author) - .def_readwrite("subject", &odr::DocumentMeta::subject) - .def_readwrite("keywords", &odr::DocumentMeta::keywords) - .def_readwrite("creator", &odr::DocumentMeta::creator) - .def_readwrite("producer", &odr::DocumentMeta::producer) - .def_readwrite("creation_date", &odr::DocumentMeta::creation_date) - .def_readwrite("modification_date", - &odr::DocumentMeta::modification_date); - py::class_(m, "FileMeta") .def(py::init<>()) .def_readwrite("type", &odr::FileMeta::type) @@ -114,7 +100,16 @@ void odr_python::bind_file(py::module_ &m) { "mimetype", [](const odr::FileMeta &meta) { return std::string(meta.mimetype); }) .def_readwrite("password_encrypted", &odr::FileMeta::password_encrypted) - .def_readwrite("document_meta", &odr::FileMeta::document_meta); + .def_readwrite("document_type", &odr::FileMeta::document_type) + .def_readwrite("entry_count", &odr::FileMeta::entry_count) + .def_readwrite("title", &odr::FileMeta::title) + .def_readwrite("author", &odr::FileMeta::author) + .def_readwrite("subject", &odr::FileMeta::subject) + .def_readwrite("keywords", &odr::FileMeta::keywords) + .def_readwrite("creator", &odr::FileMeta::creator) + .def_readwrite("producer", &odr::FileMeta::producer) + .def_readwrite("creation_date", &odr::FileMeta::creation_date) + .def_readwrite("modification_date", &odr::FileMeta::modification_date); py::class_(m, "File") .def(py::init<>()) @@ -183,7 +178,6 @@ void odr_python::bind_file(py::module_ &m) { .def_static("type_by_path", &odr::DocumentFile::type, py::arg("path")) .def_static("meta_by_path", &odr::DocumentFile::meta, py::arg("path")) .def("document_type", &odr::DocumentFile::document_type) - .def("document_meta", &odr::DocumentFile::document_meta) .def("decrypt", &odr::DocumentFile::decrypt, py::arg("password")) .def("document", &odr::DocumentFile::document); diff --git a/python/tests/test_document.py b/python/tests/test_document.py index adf0377df..677afb600 100644 --- a/python/tests/test_document.py +++ b/python/tests/test_document.py @@ -23,7 +23,7 @@ def test_open_odt(odt_path): def test_document_meta(odt_path): - meta = pyodr.open(str(odt_path)).as_document_file().document_meta() + meta = pyodr.open(str(odt_path)).as_document_file().file_meta() assert meta.document_type == pyodr.DocumentType.text diff --git a/python/tests/test_file.py b/python/tests/test_file.py index 5ab89d6ce..5d412fb5c 100644 --- a/python/tests/test_file.py +++ b/python/tests/test_file.py @@ -58,6 +58,10 @@ def test_file_meta(csv_path): meta = file.file_meta() assert meta.type == pyodr.FileType.comma_separated_values assert not meta.password_encrypted + # not a document, so the document fields stay unset + assert meta.document_type == pyodr.DocumentType.unknown + assert meta.entry_count is None + assert meta.title is None def test_open_zip_archive(odt_path): diff --git a/src/odr/file.cpp b/src/odr/file.cpp index 9d7ab0589..6d7ecae46 100644 --- a/src/odr/file.cpp +++ b/src/odr/file.cpp @@ -15,20 +15,6 @@ namespace odr { -DocumentMeta::DocumentMeta() = default; - -DocumentMeta::DocumentMeta(const DocumentType document_type, - const std::optional entry_count) - : document_type{document_type}, entry_count{entry_count} {} - -FileMeta::FileMeta() = default; - -FileMeta::FileMeta(const FileType type, const std::string_view mimetype, - const bool password_encrypted, - const std::optional document_meta) - : type{type}, mimetype{mimetype}, password_encrypted{password_encrypted}, - document_meta{document_meta} {} - File::File() = default; File::File(std::shared_ptr impl) @@ -270,10 +256,6 @@ DocumentType DocumentFile::document_type() const { return m_impl->document_type(); } -DocumentMeta DocumentFile::document_meta() const { - return m_impl->document_meta(); -} - DocumentFile DocumentFile::decrypt(const std::string &password) const { return DecodedFile::decrypt(password).as_document_file(); } diff --git a/src/odr/file.hpp b/src/odr/file.hpp index 7f3ba94f1..387755007 100644 --- a/src/odr/file.hpp +++ b/src/odr/file.hpp @@ -134,11 +134,14 @@ enum class DocumentType { drawing, }; -/// @brief Meta information about a document. -struct DocumentMeta final { - DocumentMeta(); - DocumentMeta(DocumentType document_type, - std::optional entry_count); +/// @brief Meta information about a file. +/// +/// The document fields are only meaningful when @ref document_type is set; +/// `document_type == DocumentType::unknown` means the file is not a document. +struct FileMeta final { + FileType type{FileType::unknown}; + std::string_view mimetype; + bool password_encrypted{false}; DocumentType document_type{DocumentType::unknown}; std::optional entry_count; @@ -155,18 +158,6 @@ struct DocumentMeta final { std::optional modification_date; }; -/// @brief Meta information about a file. -struct FileMeta final { - FileMeta(); - FileMeta(FileType type, std::string_view mimetype, bool password_encrypted, - std::optional document_meta); - - FileType type{FileType::unknown}; - std::string_view mimetype; - bool password_encrypted{false}; - std::optional document_meta; -}; - /// @brief Represents a file. class File final { public: @@ -291,7 +282,6 @@ class DocumentFile final : public DecodedFile { const Logger &logger = Logger::null()); [[nodiscard]] DocumentType document_type() const; - [[nodiscard]] DocumentMeta document_meta() const; [[nodiscard]] DocumentFile decrypt(const std::string &password) const; diff --git a/src/odr/internal/abstract/file.hpp b/src/odr/internal/abstract/file.hpp index 9845bbdcc..155cbe114 100644 --- a/src/odr/internal/abstract/file.hpp +++ b/src/odr/internal/abstract/file.hpp @@ -88,7 +88,6 @@ class DocumentFile : public DecodedFile { } [[nodiscard]] virtual DocumentType document_type() const = 0; - [[nodiscard]] virtual DocumentMeta document_meta() const = 0; [[nodiscard]] virtual std::shared_ptr document() const = 0; }; diff --git a/src/odr/internal/cfb/cfb_file.cpp b/src/odr/internal/cfb/cfb_file.cpp index 0b8ed17ea..317f2fce6 100644 --- a/src/odr/internal/cfb/cfb_file.cpp +++ b/src/odr/internal/cfb/cfb_file.cpp @@ -25,7 +25,8 @@ std::string_view CfbFile::mimetype() const noexcept { } FileMeta CfbFile::file_meta() const noexcept { - return {file_type(), mimetype(), false, std::nullopt}; + return { + .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; } bool CfbFile::is_decodable() const noexcept { return true; } diff --git a/src/odr/internal/csv/csv_file.cpp b/src/odr/internal/csv/csv_file.cpp index c0f5e33c7..ebfdc6074 100644 --- a/src/odr/internal/csv/csv_file.cpp +++ b/src/odr/internal/csv/csv_file.cpp @@ -25,7 +25,8 @@ FileType CsvFile::file_type() const noexcept { std::string_view CsvFile::mimetype() const noexcept { return "text/csv"; } FileMeta CsvFile::file_meta() const noexcept { - return {file_type(), mimetype(), false, std::nullopt}; + return { + .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; } bool CsvFile::is_decodable() const noexcept { return false; } diff --git a/src/odr/internal/font/font_file.cpp b/src/odr/internal/font/font_file.cpp index 8686bc049..48ac02ba4 100644 --- a/src/odr/internal/font/font_file.cpp +++ b/src/odr/internal/font/font_file.cpp @@ -34,7 +34,8 @@ std::string_view FontFile::mimetype() const noexcept { } FileMeta FontFile::file_meta() const noexcept { - return {file_type(), mimetype(), false, std::nullopt}; + return { + .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; } bool FontFile::is_decodable() const noexcept { return true; } diff --git a/src/odr/internal/json/json_file.cpp b/src/odr/internal/json/json_file.cpp index f4b287a3a..8d3792608 100644 --- a/src/odr/internal/json/json_file.cpp +++ b/src/odr/internal/json/json_file.cpp @@ -27,7 +27,8 @@ std::string_view JsonFile::mimetype() const noexcept { } FileMeta JsonFile::file_meta() const noexcept { - return {file_type(), mimetype(), false, std::nullopt}; + return { + .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; } bool JsonFile::is_decodable() const noexcept { return false; } diff --git a/src/odr/internal/odf/odf_file.cpp b/src/odr/internal/odf/odf_file.cpp index 01c2cd2cd..0893e60a3 100644 --- a/src/odr/internal/odf/odf_file.cpp +++ b/src/odr/internal/odf/odf_file.cpp @@ -51,15 +51,7 @@ std::string_view OpenDocumentFile::mimetype() const noexcept { FileMeta OpenDocumentFile::file_meta() const noexcept { return m_file_meta; } DocumentType OpenDocumentFile::document_type() const { - // document_meta is always set for document files - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - return m_file_meta.document_meta.value().document_type; -} - -DocumentMeta OpenDocumentFile::document_meta() const { - // document_meta is always set for document files - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - return m_file_meta.document_meta.value(); + return m_file_meta.document_type; } bool OpenDocumentFile::password_encrypted() const noexcept { diff --git a/src/odr/internal/odf/odf_file.hpp b/src/odr/internal/odf/odf_file.hpp index 77a6e94c2..aa48e3190 100644 --- a/src/odr/internal/odf/odf_file.hpp +++ b/src/odr/internal/odf/odf_file.hpp @@ -31,7 +31,6 @@ class OpenDocumentFile final : public virtual abstract::DocumentFile { [[nodiscard]] FileMeta file_meta() const noexcept override; [[nodiscard]] DocumentType document_type() const override; - [[nodiscard]] DocumentMeta document_meta() const override; [[nodiscard]] bool password_encrypted() const noexcept override; [[nodiscard]] EncryptionState encryption_state() const noexcept override; diff --git a/src/odr/internal/odf/odf_meta.cpp b/src/odr/internal/odf/odf_meta.cpp index b75386672..3d9b6ef56 100644 --- a/src/odr/internal/odf/odf_meta.cpp +++ b/src/odr/internal/odf/odf_meta.cpp @@ -106,16 +106,14 @@ FileMeta parse_file_meta(const abstract::ReadableFilesystem &filesystem, } } - DocumentMeta document_meta; - if (result.type == FileType::opendocument_text) { - document_meta.document_type = DocumentType::text; + result.document_type = DocumentType::text; } else if (result.type == FileType::opendocument_presentation) { - document_meta.document_type = DocumentType::presentation; + result.document_type = DocumentType::presentation; } else if (result.type == FileType::opendocument_spreadsheet) { - document_meta.document_type = DocumentType::spreadsheet; + result.document_type = DocumentType::spreadsheet; } else if (result.type == FileType::opendocument_graphics) { - document_meta.document_type = DocumentType::drawing; + result.document_type = DocumentType::drawing; } if (result.password_encrypted == decrypted && @@ -130,18 +128,16 @@ FileMeta parse_file_meta(const abstract::ReadableFilesystem &filesystem, if (result.type == FileType::opendocument_text) { if (const pugi::xml_attribute page_count = statistics.attribute("meta:page-count")) { - document_meta.entry_count = page_count.as_uint(); + result.entry_count = page_count.as_uint(); } } else if (result.type == FileType::opendocument_spreadsheet) { if (const pugi::xml_attribute table_count = statistics.attribute("meta:table-count")) { - document_meta.entry_count = table_count.as_uint(); + result.entry_count = table_count.as_uint(); } } } - result.document_meta = document_meta; - return result; } diff --git a/src/odr/internal/oldms/oldms_file.cpp b/src/odr/internal/oldms/oldms_file.cpp index feac89622..2c7464aaf 100644 --- a/src/odr/internal/oldms/oldms_file.cpp +++ b/src/odr/internal/oldms/oldms_file.cpp @@ -39,13 +39,12 @@ FileMeta parse_meta(const abstract::ReadableFilesystem &files) { }; FileMeta result; - result.document_meta = DocumentMeta(); for (const auto &[path, variant] : types) { if (files.is_file(path)) { result.type = variant.type; result.mimetype = variant.mimetype; - result.document_meta->document_type = variant.document_type; + result.document_type = variant.document_type; break; } } @@ -83,15 +82,7 @@ std::string_view LegacyMicrosoftFile::mimetype() const noexcept { FileMeta LegacyMicrosoftFile::file_meta() const noexcept { return m_file_meta; } DocumentType LegacyMicrosoftFile::document_type() const { - // document_meta is always set for document files - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - return m_file_meta.document_meta.value().document_type; -} - -DocumentMeta LegacyMicrosoftFile::document_meta() const { - // document_meta is always set for document files - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - return m_file_meta.document_meta.value(); + return m_file_meta.document_type; } bool LegacyMicrosoftFile::password_encrypted() const noexcept { diff --git a/src/odr/internal/oldms/oldms_file.hpp b/src/odr/internal/oldms/oldms_file.hpp index 8e8f5f416..66e8f4f13 100644 --- a/src/odr/internal/oldms/oldms_file.hpp +++ b/src/odr/internal/oldms/oldms_file.hpp @@ -28,7 +28,6 @@ class LegacyMicrosoftFile final : public abstract::DocumentFile { [[nodiscard]] FileMeta file_meta() const noexcept override; [[nodiscard]] DocumentType document_type() const override; - [[nodiscard]] DocumentMeta document_meta() const override; [[nodiscard]] bool password_encrypted() const noexcept override; [[nodiscard]] EncryptionState encryption_state() const noexcept override; diff --git a/src/odr/internal/ooxml/ooxml_file.cpp b/src/odr/internal/ooxml/ooxml_file.cpp index 923e63a49..16cae8bbb 100644 --- a/src/odr/internal/ooxml/ooxml_file.cpp +++ b/src/odr/internal/ooxml/ooxml_file.cpp @@ -44,15 +44,7 @@ std::string_view OfficeOpenXmlFile::mimetype() const noexcept { FileMeta OfficeOpenXmlFile::file_meta() const noexcept { return m_file_meta; } DocumentType OfficeOpenXmlFile::document_type() const { - // document_meta is always set for document files - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - return m_file_meta.document_meta.value().document_type; -} - -DocumentMeta OfficeOpenXmlFile::document_meta() const { - // document_meta is always set for document files - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - return m_file_meta.document_meta.value(); + return m_file_meta.document_type; } bool OfficeOpenXmlFile::password_encrypted() const noexcept { diff --git a/src/odr/internal/ooxml/ooxml_file.hpp b/src/odr/internal/ooxml/ooxml_file.hpp index e166b5755..d7cb79aa9 100644 --- a/src/odr/internal/ooxml/ooxml_file.hpp +++ b/src/odr/internal/ooxml/ooxml_file.hpp @@ -30,7 +30,6 @@ class OfficeOpenXmlFile final : public abstract::DocumentFile { [[nodiscard]] FileMeta file_meta() const noexcept override; [[nodiscard]] DocumentType document_type() const override; - [[nodiscard]] DocumentMeta document_meta() const override; [[nodiscard]] bool password_encrypted() const noexcept override; [[nodiscard]] EncryptionState encryption_state() const noexcept override; diff --git a/src/odr/internal/ooxml/ooxml_meta.cpp b/src/odr/internal/ooxml/ooxml_meta.cpp index 39241deab..c34ba388a 100644 --- a/src/odr/internal/ooxml/ooxml_meta.cpp +++ b/src/odr/internal/ooxml/ooxml_meta.cpp @@ -32,7 +32,6 @@ FileMeta parse_file_meta(const abstract::ReadableFilesystem &files) { }; FileMeta result; - result.document_meta = DocumentMeta(); if (files.is_file(AbsPath("/EncryptionInfo")) && files.is_file(AbsPath("/EncryptedPackage"))) { @@ -45,7 +44,7 @@ FileMeta parse_file_meta(const abstract::ReadableFilesystem &files) { if (files.is_file(path)) { result.type = info.file_type; result.mimetype = info.mimetype; - result.document_meta->document_type = info.document_type; + result.document_type = info.document_type; break; } } diff --git a/src/odr/internal/pdf/AGENTS.md b/src/odr/internal/pdf/AGENTS.md index 63a4b1eb5..cb566d0e8 100644 --- a/src/odr/internal/pdf/AGENTS.md +++ b/src/odr/internal/pdf/AGENTS.md @@ -143,10 +143,11 @@ above their text). So each run is raised by one font ascent: Things the code won't shout at you: - **`is_decodable()` returns `false`** for PDF; page-tree/content parsing is lazy - (on HTML request). But `file_meta()` still carries a best-effort `document_meta` + (on HTML request). But `file_meta()` still carries best-effort document metadata (page count + `/Info` strings), read once at construction (an owner-locked file - is unlocked with the empty password first so `/Info` decrypts). XMP is not - parsed — `document_meta` is `/Info`-only. + is unlocked with the empty password first so `/Info` decrypts). It is + all-or-nothing: a malformed structure leaves `document_type` at `unknown` rather + than half-filling the fields. XMP is not parsed — the strings are `/Info`-only. - **Image codecs are deliberately not decoded** in the filter framework (DCTDecode/JPXDecode/CCITTFaxDecode/JBIG2Decode): `decode()` stops and hands back the still-encoded payload for the image path; `read_decoded_stream` treats diff --git a/src/odr/internal/pdf/pdf_file.cpp b/src/odr/internal/pdf/pdf_file.cpp index 58532d502..3ed30a46b 100644 --- a/src/odr/internal/pdf/pdf_file.cpp +++ b/src/odr/internal/pdf/pdf_file.cpp @@ -41,8 +41,7 @@ std::optional info_string(DocumentParser &parser, /// /Count`, a single lookup — no tree walk) and the `/Info` document /// information dictionary (ISO 32000-1 14.3.3). Reads may throw on a malformed /// file; the caller treats that as "no metadata". -DocumentMeta parse_document_meta(DocumentParser &parser) { - DocumentMeta meta; +void parse_document_meta(DocumentParser &parser, FileMeta &meta) { meta.document_type = DocumentType::text; const Dictionary &trailer = parser.trailer(); @@ -76,17 +75,18 @@ DocumentMeta parse_document_meta(DocumentParser &parser) { meta.modification_date = info_string(parser, d, "ModDate"); } } - - return meta; } -/// Fill `meta.document_meta` best-effort (page count + `/Info`) from a readable -/// parser. Never fatal: a malformed structure simply leaves it unset. +/// Fill the document fields of `meta` best-effort (page count + `/Info`) from a +/// readable parser. Never fatal, and all-or-nothing: a malformed structure +/// leaves every document field unset rather than half-filled. void populate_document_meta(DocumentParser &parser, FileMeta &meta) { try { - meta.document_meta = parse_document_meta(parser); + FileMeta parsed = meta; + parse_document_meta(parser, parsed); + meta = std::move(parsed); } catch (...) { // NOLINT(bugprone-empty-catch): metadata is best-effort, a - // malformed file simply carries no `document_meta`. + // malformed file simply carries no document metadata. } } diff --git a/src/odr/internal/svm/svm_file.cpp b/src/odr/internal/svm/svm_file.cpp index fba4ab14b..8b50f49b4 100644 --- a/src/odr/internal/svm/svm_file.cpp +++ b/src/odr/internal/svm/svm_file.cpp @@ -36,7 +36,8 @@ std::string_view SvmFile::mimetype() const noexcept { } FileMeta SvmFile::file_meta() const noexcept { - return {file_type(), mimetype(), false, std::nullopt}; + return { + .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; } bool SvmFile::is_decodable() const noexcept { return false; } diff --git a/src/odr/internal/text/text_file.cpp b/src/odr/internal/text/text_file.cpp index 521bfe6fd..b2527c5c6 100644 --- a/src/odr/internal/text/text_file.cpp +++ b/src/odr/internal/text/text_file.cpp @@ -23,7 +23,8 @@ FileType TextFile::file_type() const noexcept { return FileType::text_file; } std::string_view TextFile::mimetype() const noexcept { return "text/plain"; } FileMeta TextFile::file_meta() const noexcept { - return {file_type(), mimetype(), false, std::nullopt}; + return { + .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; } bool TextFile::is_decodable() const noexcept { return false; } diff --git a/src/odr/internal/util/odr_meta_util.cpp b/src/odr/internal/util/odr_meta_util.cpp index 25d37e3bd..88f2dd1ca 100644 --- a/src/odr/internal/util/odr_meta_util.cpp +++ b/src/odr/internal/util/odr_meta_util.cpp @@ -15,14 +15,11 @@ nlohmann::json meta_to_json(const FileMeta &meta) { file_category_to_string(file_category_by_file_type(meta.type)); result["isEncrypted"] = meta.password_encrypted; - if (meta.document_meta) { - const auto &document_meta = *meta.document_meta; + if (meta.document_type != DocumentType::unknown) { + result["documentType"] = document_type_to_string(meta.document_type); - result["documentType"] = - document_type_to_string(document_meta.document_type); - - if (document_meta.entry_count) { - result["entryCount"] = *document_meta.entry_count; + if (meta.entry_count) { + result["entryCount"] = *meta.entry_count; } const auto put = [&](const char *key, @@ -31,14 +28,14 @@ nlohmann::json meta_to_json(const FileMeta &meta) { result[key] = *value; } }; - put("title", document_meta.title); - put("author", document_meta.author); - put("subject", document_meta.subject); - put("keywords", document_meta.keywords); - put("creator", document_meta.creator); - put("producer", document_meta.producer); - put("creationDate", document_meta.creation_date); - put("modificationDate", document_meta.modification_date); + put("title", meta.title); + put("author", meta.author); + put("subject", meta.subject); + put("keywords", meta.keywords); + put("creator", meta.creator); + put("producer", meta.producer); + put("creationDate", meta.creation_date); + put("modificationDate", meta.modification_date); } return result; diff --git a/src/odr/internal/zip/zip_file.cpp b/src/odr/internal/zip/zip_file.cpp index 30c6a5474..5890176a6 100644 --- a/src/odr/internal/zip/zip_file.cpp +++ b/src/odr/internal/zip/zip_file.cpp @@ -23,7 +23,8 @@ std::string_view ZipFile::mimetype() const noexcept { } FileMeta ZipFile::file_meta() const noexcept { - return {file_type(), mimetype(), false, std::nullopt}; + return { + .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; } bool ZipFile::is_decodable() const noexcept { return true; } diff --git a/test/src/html_output_test.cpp b/test/src/html_output_test.cpp index eb52236df..8958983ff 100644 --- a/test/src/html_output_test.cpp +++ b/test/src/html_output_test.cpp @@ -80,8 +80,7 @@ TEST_P(HtmlOutputTests, html_meta) { EXPECT_EQ(file_meta.type, expected_file_type_pre_decryption(test_file)); if (file_category == FileCategory::document) { - EXPECT_TRUE(file_meta.document_meta.has_value()); - EXPECT_EQ(file_meta.document_meta->document_type, + EXPECT_EQ(file_meta.document_type, document_type_by_file_type( expected_file_type_pre_decryption(test_file))); } @@ -128,8 +127,7 @@ TEST_P(HtmlOutputTests, html_meta) { EXPECT_EQ(file_meta.type, expected_file_type_post_decryption(test_file)); if (file_category == FileCategory::document) { - EXPECT_TRUE(file_meta.document_meta.has_value()); - EXPECT_EQ(file_meta.document_meta->document_type, + EXPECT_EQ(file_meta.document_type, document_type_by_file_type( expected_file_type_post_decryption(test_file))); } diff --git a/test/src/internal/pdf/pdf_file.cpp b/test/src/internal/pdf/pdf_file.cpp index e30044283..35b9aab36 100644 --- a/test/src/internal/pdf/pdf_file.cpp +++ b/test/src/internal/pdf/pdf_file.cpp @@ -89,27 +89,24 @@ std::string info_mini_pdf() { } // namespace // `/Info` document-information strings and the page count surface through -// `file_meta().document_meta`; a `/Title` UTF-16BE string is decoded to UTF-8. +// `file_meta()`; a `/Title` UTF-16BE string is decoded to UTF-8. TEST(PdfFile, file_meta_carries_info_and_page_count) { const std::shared_ptr file = open_pdf(info_mini_pdf()); const FileMeta meta = file->file_meta(); EXPECT_EQ(meta.type, FileType::portable_document_format); - ASSERT_TRUE(meta.document_meta.has_value()); - const DocumentMeta &document_meta = *meta.document_meta; - - EXPECT_EQ(document_meta.document_type, DocumentType::text); - ASSERT_TRUE(document_meta.entry_count.has_value()); - EXPECT_EQ(*document_meta.entry_count, 2u); - - ASSERT_TRUE(document_meta.title.has_value()); - EXPECT_EQ(*document_meta.title, "HI"); - ASSERT_TRUE(document_meta.author.has_value()); - EXPECT_EQ(*document_meta.author, "Ada Lovelace"); - ASSERT_TRUE(document_meta.producer.has_value()); - EXPECT_EQ(*document_meta.producer, "odr-test"); - EXPECT_FALSE(document_meta.subject.has_value()); - EXPECT_FALSE(document_meta.keywords.has_value()); + EXPECT_EQ(meta.document_type, DocumentType::text); + ASSERT_TRUE(meta.entry_count.has_value()); + EXPECT_EQ(*meta.entry_count, 2u); + + ASSERT_TRUE(meta.title.has_value()); + EXPECT_EQ(*meta.title, "HI"); + ASSERT_TRUE(meta.author.has_value()); + EXPECT_EQ(*meta.author, "Ada Lovelace"); + ASSERT_TRUE(meta.producer.has_value()); + EXPECT_EQ(*meta.producer, "odr-test"); + EXPECT_FALSE(meta.subject.has_value()); + EXPECT_FALSE(meta.keywords.has_value()); } // A file with no `/Info` still reports its page count; the `/Info` strings stay @@ -124,12 +121,11 @@ TEST(PdfFile, file_meta_without_info) { const std::shared_ptr file = open_pdf(pdf); const FileMeta meta = file->file_meta(); - ASSERT_TRUE(meta.document_meta.has_value()); - EXPECT_EQ(meta.document_meta->document_type, DocumentType::text); - ASSERT_TRUE(meta.document_meta->entry_count.has_value()); - EXPECT_EQ(*meta.document_meta->entry_count, 1u); - EXPECT_FALSE(meta.document_meta->title.has_value()); - EXPECT_FALSE(meta.document_meta->author.has_value()); + EXPECT_EQ(meta.document_type, DocumentType::text); + ASSERT_TRUE(meta.entry_count.has_value()); + EXPECT_EQ(*meta.entry_count, 1u); + EXPECT_FALSE(meta.title.has_value()); + EXPECT_FALSE(meta.author.has_value()); } // `/Link` annotations render as `` overlays: a `/URI` action → external href From a3786e9f2987455e20ccb8c8c0c0c830ee6c532a Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 27 Jul 2026 21:09:46 +0200 Subject: [PATCH 2/3] fix: build the flat `FileMeta` without partial aggregate init GCC rejects the designated initializers that name only the first three members under `-Werror=missing-field-initializers`, even though the rest carry default member initializers. Build the value field by field instead, matching how `odf_meta` / `ooxml_meta` / `oldms_file` already do it. Also spell out why the JSON document block is gated on `document_type`: an office container we cannot name a type for (an encrypted OOXML package, an ODF file with an unrecognized mimetype) now omits those keys instead of reporting `"documentType": "unknown"`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KbnNCsz6rkVKHW2MWL47m5 --- src/odr/internal/cfb/cfb_file.cpp | 6 ++++-- src/odr/internal/csv/csv_file.cpp | 6 ++++-- src/odr/internal/font/font_file.cpp | 6 ++++-- src/odr/internal/json/json_file.cpp | 6 ++++-- src/odr/internal/svm/svm_file.cpp | 6 ++++-- src/odr/internal/text/text_file.cpp | 6 ++++-- src/odr/internal/util/odr_meta_util.cpp | 4 ++++ src/odr/internal/zip/zip_file.cpp | 6 ++++-- 8 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/odr/internal/cfb/cfb_file.cpp b/src/odr/internal/cfb/cfb_file.cpp index 317f2fce6..baaceb69e 100644 --- a/src/odr/internal/cfb/cfb_file.cpp +++ b/src/odr/internal/cfb/cfb_file.cpp @@ -25,8 +25,10 @@ std::string_view CfbFile::mimetype() const noexcept { } FileMeta CfbFile::file_meta() const noexcept { - return { - .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; + FileMeta result; + result.type = file_type(); + result.mimetype = mimetype(); + return result; } bool CfbFile::is_decodable() const noexcept { return true; } diff --git a/src/odr/internal/csv/csv_file.cpp b/src/odr/internal/csv/csv_file.cpp index ebfdc6074..110c5da84 100644 --- a/src/odr/internal/csv/csv_file.cpp +++ b/src/odr/internal/csv/csv_file.cpp @@ -25,8 +25,10 @@ FileType CsvFile::file_type() const noexcept { std::string_view CsvFile::mimetype() const noexcept { return "text/csv"; } FileMeta CsvFile::file_meta() const noexcept { - return { - .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; + FileMeta result; + result.type = file_type(); + result.mimetype = mimetype(); + return result; } bool CsvFile::is_decodable() const noexcept { return false; } diff --git a/src/odr/internal/font/font_file.cpp b/src/odr/internal/font/font_file.cpp index 48ac02ba4..98e359875 100644 --- a/src/odr/internal/font/font_file.cpp +++ b/src/odr/internal/font/font_file.cpp @@ -34,8 +34,10 @@ std::string_view FontFile::mimetype() const noexcept { } FileMeta FontFile::file_meta() const noexcept { - return { - .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; + FileMeta result; + result.type = file_type(); + result.mimetype = mimetype(); + return result; } bool FontFile::is_decodable() const noexcept { return true; } diff --git a/src/odr/internal/json/json_file.cpp b/src/odr/internal/json/json_file.cpp index 8d3792608..3aab589e6 100644 --- a/src/odr/internal/json/json_file.cpp +++ b/src/odr/internal/json/json_file.cpp @@ -27,8 +27,10 @@ std::string_view JsonFile::mimetype() const noexcept { } FileMeta JsonFile::file_meta() const noexcept { - return { - .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; + FileMeta result; + result.type = file_type(); + result.mimetype = mimetype(); + return result; } bool JsonFile::is_decodable() const noexcept { return false; } diff --git a/src/odr/internal/svm/svm_file.cpp b/src/odr/internal/svm/svm_file.cpp index 8b50f49b4..89f7488cb 100644 --- a/src/odr/internal/svm/svm_file.cpp +++ b/src/odr/internal/svm/svm_file.cpp @@ -36,8 +36,10 @@ std::string_view SvmFile::mimetype() const noexcept { } FileMeta SvmFile::file_meta() const noexcept { - return { - .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; + FileMeta result; + result.type = file_type(); + result.mimetype = mimetype(); + return result; } bool SvmFile::is_decodable() const noexcept { return false; } diff --git a/src/odr/internal/text/text_file.cpp b/src/odr/internal/text/text_file.cpp index b2527c5c6..0f06cf129 100644 --- a/src/odr/internal/text/text_file.cpp +++ b/src/odr/internal/text/text_file.cpp @@ -23,8 +23,10 @@ FileType TextFile::file_type() const noexcept { return FileType::text_file; } std::string_view TextFile::mimetype() const noexcept { return "text/plain"; } FileMeta TextFile::file_meta() const noexcept { - return { - .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; + FileMeta result; + result.type = file_type(); + result.mimetype = mimetype(); + return result; } bool TextFile::is_decodable() const noexcept { return false; } diff --git a/src/odr/internal/util/odr_meta_util.cpp b/src/odr/internal/util/odr_meta_util.cpp index 88f2dd1ca..c835d976d 100644 --- a/src/odr/internal/util/odr_meta_util.cpp +++ b/src/odr/internal/util/odr_meta_util.cpp @@ -15,6 +15,10 @@ nlohmann::json meta_to_json(const FileMeta &meta) { file_category_to_string(file_category_by_file_type(meta.type)); result["isEncrypted"] = meta.password_encrypted; + // Only a known document type gets the document block. An office container we + // cannot name a type for (an encrypted OOXML package, an ODF file with an + // unrecognized mimetype) is therefore treated like any other non-document and + // omits these keys, rather than reporting `"documentType": "unknown"`. if (meta.document_type != DocumentType::unknown) { result["documentType"] = document_type_to_string(meta.document_type); diff --git a/src/odr/internal/zip/zip_file.cpp b/src/odr/internal/zip/zip_file.cpp index 5890176a6..aac32eb33 100644 --- a/src/odr/internal/zip/zip_file.cpp +++ b/src/odr/internal/zip/zip_file.cpp @@ -23,8 +23,10 @@ std::string_view ZipFile::mimetype() const noexcept { } FileMeta ZipFile::file_meta() const noexcept { - return { - .type = file_type(), .mimetype = mimetype(), .password_encrypted = false}; + FileMeta result; + result.type = file_type(); + result.mimetype = mimetype(); + return result; } bool ZipFile::is_decodable() const noexcept { return true; } From 6300a728fccc360c3641a8b401ea5526419a021f Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 27 Jul 2026 21:50:00 +0200 Subject: [PATCH 3/3] test: advance the reference output for the flat `FileMeta` An encrypted OOXML package no longer reports `"documentType": "unknown"`, since `document_type != unknown` is now the "is a document" signal and the backends no longer materialise a default `DocumentMeta` on the way out. The two affected outputs are the only ones that changed (326 of 328 matched); no HTML moved. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KbnNCsz6rkVKHW2MWL47m5 --- test/data/reference-output/odr-public | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data/reference-output/odr-public b/test/data/reference-output/odr-public index 4b899126f..e74d3332c 160000 --- a/test/data/reference-output/odr-public +++ b/test/data/reference-output/odr-public @@ -1 +1 @@ -Subproject commit 4b899126fb0e228c5997802b94fdb504a59b72a3 +Subproject commit e74d3332c7ef2d0537cfc51f93ea59cc3e977555