Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ The release run heads these entries with the version and opens a fresh
no run, as a new odt or docx renders. Typing there went nowhere and said
nothing.

### API, files and saving

- `DecodedFile::file()` returns the file an odf, ooxml, iWork or legacy Office
package was read from. It returned an empty handle, so `file().name()` threw
`NullPointerError`, and so did `fileName` in npm for every such document.

## v7.5.1 - 2026-10-10

### Spreadsheets
Expand Down
9 changes: 6 additions & 3 deletions src/odr/internal/iwork/iwork_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ FileType parse_file_type(const abstract::ReadableFilesystem &filesystem) {

} // namespace

IworkFile::IworkFile(std::shared_ptr<abstract::ReadableFilesystem> filesystem)
: m_filesystem{std::move(filesystem)} {
IworkFile::IworkFile(std::shared_ptr<abstract::ReadableFilesystem> filesystem,
std::shared_ptr<abstract::File> source)
: m_filesystem{std::move(filesystem)}, m_source{std::move(source)} {
if (!m_filesystem->is_file(AbsPath("/Index/Document.iwa"))) {
throw NoIworkFile();
}
Expand All @@ -70,7 +71,9 @@ IworkFile::IworkFile(std::shared_ptr<abstract::ReadableFilesystem> filesystem)
m_file_meta.document_type = document_type_by_file_type(m_file_meta.type);
}

std::shared_ptr<abstract::File> IworkFile::file() const noexcept { return {}; }
std::shared_ptr<abstract::File> IworkFile::file() const noexcept {
return m_source;
}

FileType IworkFile::file_type() const noexcept { return m_file_meta.type; }

Expand Down
5 changes: 4 additions & 1 deletion src/odr/internal/iwork/iwork_file.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ namespace odr::internal::iwork {
/// An iWork package, detected from its archives and component list.
class IworkFile final : public abstract::DocumentFile {
public:
explicit IworkFile(std::shared_ptr<abstract::ReadableFilesystem> filesystem);
/// @p source is the file the package was read from, if any.
explicit IworkFile(std::shared_ptr<abstract::ReadableFilesystem> filesystem,
std::shared_ptr<abstract::File> source = nullptr);

[[nodiscard]] std::shared_ptr<abstract::File> file() const noexcept override;

Expand All @@ -33,6 +35,7 @@ class IworkFile final : public abstract::DocumentFile {

private:
std::shared_ptr<abstract::ReadableFilesystem> m_filesystem;
std::shared_ptr<abstract::File> m_source;
FileMeta m_file_meta;
};

Expand Down
8 changes: 5 additions & 3 deletions src/odr/internal/odf/odf_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ namespace odr::internal::odf {

OpenDocumentFile::OpenDocumentFile(
std::shared_ptr<abstract::ReadableFilesystem> filesystem,
std::shared_ptr<zip::util::Archive> archive)
: m_filesystem{std::move(filesystem)}, m_archive{std::move(archive)} {
std::shared_ptr<zip::util::Archive> archive,
std::shared_ptr<abstract::File> source)
: m_filesystem{std::move(filesystem)}, m_archive{std::move(archive)},
m_source{std::move(source)} {
if (m_filesystem->exists(AbsPath("/META-INF/manifest.xml"))) {
const pugi::xml_document manifest =
xml::parse(*m_filesystem, AbsPath("/META-INF/manifest.xml"));
Expand All @@ -34,7 +36,7 @@ OpenDocumentFile::OpenDocumentFile(
}

std::shared_ptr<abstract::File> OpenDocumentFile::file() const noexcept {
return {};
return m_source;
}

FileType OpenDocumentFile::file_type() const noexcept {
Expand Down
5 changes: 4 additions & 1 deletion src/odr/internal/odf/odf_file.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ namespace odr::internal::odf {
class OpenDocumentFile final : public virtual abstract::DocumentFile {
public:
/// @p archive is the package @p filesystem reads from lazily, if any.
/// @p source is the file the package was read from, if any.
explicit OpenDocumentFile(
std::shared_ptr<abstract::ReadableFilesystem> filesystem,
std::shared_ptr<zip::util::Archive> archive = nullptr);
std::shared_ptr<zip::util::Archive> archive = nullptr,
std::shared_ptr<abstract::File> source = nullptr);

[[nodiscard]] std::shared_ptr<abstract::File> file() const noexcept override;

Expand All @@ -51,6 +53,7 @@ class OpenDocumentFile final : public virtual abstract::DocumentFile {
private:
std::shared_ptr<abstract::ReadableFilesystem> m_filesystem;
std::shared_ptr<zip::util::Archive> m_archive;
std::shared_ptr<abstract::File> m_source;
EncryptionState m_encryption_state{EncryptionState::not_encrypted};
FileMeta m_file_meta;
Manifest m_manifest;
Expand Down
7 changes: 4 additions & 3 deletions src/odr/internal/oldms/oldms_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ FileMeta parse_meta(const abstract::ReadableFilesystem &files) {
} // namespace

LegacyMicrosoftFile::LegacyMicrosoftFile(
std::shared_ptr<abstract::ReadableFilesystem> files)
: m_files{std::move(files)} {
std::shared_ptr<abstract::ReadableFilesystem> files,
std::shared_ptr<abstract::File> source)
: m_files{std::move(files)}, m_source{std::move(source)} {
m_file_meta = parse_meta(*m_files);

// An unreadable encryption marker leaves the state unknown.
Expand All @@ -79,7 +80,7 @@ LegacyMicrosoftFile::LegacyMicrosoftFile(
}

std::shared_ptr<abstract::File> LegacyMicrosoftFile::file() const noexcept {
return {};
return m_source;
}

FileType LegacyMicrosoftFile::file_type() const noexcept {
Expand Down
5 changes: 4 additions & 1 deletion src/odr/internal/oldms/oldms_file.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ namespace odr::internal::oldms {

class LegacyMicrosoftFile final : public abstract::DocumentFile {
public:
/// @p source is the file the compound file was read from, if any.
explicit LegacyMicrosoftFile(
std::shared_ptr<abstract::ReadableFilesystem> files);
std::shared_ptr<abstract::ReadableFilesystem> files,
std::shared_ptr<abstract::File> source = nullptr);

[[nodiscard]] std::shared_ptr<abstract::File> file() const noexcept override;

Expand All @@ -39,6 +41,7 @@ class LegacyMicrosoftFile final : public abstract::DocumentFile {

private:
std::shared_ptr<abstract::ReadableFilesystem> m_files;
std::shared_ptr<abstract::File> m_source;
FileMeta m_file_meta;
EncryptionState m_encryption_state{EncryptionState::unknown};
};
Expand Down
8 changes: 5 additions & 3 deletions src/odr/internal/ooxml/ooxml_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ namespace odr::internal::ooxml {

OfficeOpenXmlFile::OfficeOpenXmlFile(
std::shared_ptr<abstract::ReadableFilesystem> files,
std::shared_ptr<zip::util::Archive> archive)
: m_files(std::move(files)), m_archive(std::move(archive)) {
std::shared_ptr<zip::util::Archive> archive,
std::shared_ptr<abstract::File> source)
: m_files(std::move(files)), m_archive(std::move(archive)),
m_source(std::move(source)) {
m_file_meta = parse_file_meta(*m_files);

if (m_file_meta.password_encrypted) {
Expand All @@ -28,7 +30,7 @@ OfficeOpenXmlFile::OfficeOpenXmlFile(
}

std::shared_ptr<abstract::File> OfficeOpenXmlFile::file() const noexcept {
return {};
return m_source;
}

FileType OfficeOpenXmlFile::file_type() const noexcept {
Expand Down
5 changes: 4 additions & 1 deletion src/odr/internal/ooxml/ooxml_file.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ namespace odr::internal::ooxml {
class OfficeOpenXmlFile final : public abstract::DocumentFile {
public:
/// @p archive is the package @p files reads from lazily, if any.
/// @p source is the file the package was read from, if any.
explicit OfficeOpenXmlFile(
std::shared_ptr<abstract::ReadableFilesystem> files,
std::shared_ptr<zip::util::Archive> archive = nullptr);
std::shared_ptr<zip::util::Archive> archive = nullptr,
std::shared_ptr<abstract::File> source = nullptr);

[[nodiscard]] std::shared_ptr<abstract::File> file() const noexcept override;

Expand All @@ -50,6 +52,7 @@ class OfficeOpenXmlFile final : public abstract::DocumentFile {
private:
std::shared_ptr<abstract::ReadableFilesystem> m_files;
std::shared_ptr<zip::util::Archive> m_archive;
std::shared_ptr<abstract::File> m_source;
FileMeta m_file_meta;
EncryptionState m_encryption_state{EncryptionState::not_encrypted};
};
Expand Down
24 changes: 13 additions & 11 deletions src/odr/internal/open_strategy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ open_file_as(const std::shared_ptr<abstract::File> &file, const FileType as,
try {
auto zip_file = std::make_unique<zip::ZipFile>(file);
auto filesystem = zip_file->archive()->as_filesystem();
auto odf_file =
std::make_unique<odf::OpenDocumentFile>(filesystem, zip_file->zip());
auto odf_file = std::make_unique<odf::OpenDocumentFile>(
filesystem, zip_file->zip(), file);
if (odf_file->file_type() == as) {
return odf_file;
}
Expand Down Expand Up @@ -116,7 +116,7 @@ open_file_as(const std::shared_ptr<abstract::File> &file, const FileType as,
try {
auto zip_file = std::make_unique<zip::ZipFile>(file);
auto filesystem = zip_file->archive()->as_filesystem();
auto iwork_file = std::make_unique<iwork::IworkFile>(filesystem);
auto iwork_file = std::make_unique<iwork::IworkFile>(filesystem, file);
if (iwork_file->file_type() == as) {
return iwork_file;
}
Expand All @@ -136,7 +136,7 @@ open_file_as(const std::shared_ptr<abstract::File> &file, const FileType as,
auto zip_file = std::make_unique<zip::ZipFile>(file);
auto filesystem = zip_file->archive()->as_filesystem();
auto ooxml_file = std::make_unique<ooxml::OfficeOpenXmlFile>(
filesystem, zip_file->zip());
filesystem, zip_file->zip(), file);
if (is_the_requested_ooxml(*ooxml_file, as)) {
return ooxml_file;
}
Expand All @@ -147,7 +147,8 @@ open_file_as(const std::shared_ptr<abstract::File> &file, const FileType as,
try {
auto cfb_file = std::make_unique<cfb::CfbFile>(file);
auto filesystem = cfb_file->archive()->as_filesystem();
auto ooxml_file = std::make_unique<ooxml::OfficeOpenXmlFile>(filesystem);
auto ooxml_file =
std::make_unique<ooxml::OfficeOpenXmlFile>(filesystem, nullptr, file);
if (is_the_requested_ooxml(*ooxml_file, as)) {
return ooxml_file;
}
Expand All @@ -166,7 +167,7 @@ open_file_as(const std::shared_ptr<abstract::File> &file, const FileType as,
auto cfb_file = std::make_unique<cfb::CfbFile>(file);
auto filesystem = cfb_file->archive()->as_filesystem();
auto oldms_file =
std::make_unique<oldms::LegacyMicrosoftFile>(filesystem);
std::make_unique<oldms::LegacyMicrosoftFile>(filesystem, file);
if (oldms_file->file_type() == as) {
return oldms_file;
}
Expand Down Expand Up @@ -484,22 +485,22 @@ open_by_cascade(const std::shared_ptr<abstract::File> &file,
try {
ODR_VERBOSE(logger, "try open as odf");
return std::make_unique<odf::OpenDocumentFile>(filesystem,
zip_file->zip());
zip_file->zip(), file);
} catch (...) {
ODR_VERBOSE(logger, "failed to open as odf");
}

try {
ODR_VERBOSE(logger, "try open as ooxml");
return std::make_unique<ooxml::OfficeOpenXmlFile>(filesystem,
zip_file->zip());
zip_file->zip(), file);
} catch (...) {
ODR_VERBOSE(logger, "failed to open as ooxml");
}

try {
ODR_VERBOSE(logger, "try open as iwork");
return std::make_unique<iwork::IworkFile>(filesystem);
return std::make_unique<iwork::IworkFile>(filesystem, file);
} catch (...) {
ODR_VERBOSE(logger, "failed to open as iwork");
}
Expand All @@ -515,14 +516,15 @@ open_by_cascade(const std::shared_ptr<abstract::File> &file,

try {
ODR_VERBOSE(logger, "try open as legacy ms");
return std::make_unique<oldms::LegacyMicrosoftFile>(filesystem);
return std::make_unique<oldms::LegacyMicrosoftFile>(filesystem, file);
} catch (...) {
ODR_VERBOSE(logger, "failed to open as legacy ms");
}

try {
ODR_VERBOSE(logger, "try open as ooxml");
return std::make_unique<ooxml::OfficeOpenXmlFile>(filesystem);
return std::make_unique<ooxml::OfficeOpenXmlFile>(filesystem, nullptr,
file);
} catch (...) {
ODR_VERBOSE(logger, "failed to open as ooxml");
}
Expand Down
45 changes: 45 additions & 0 deletions test/src/file_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,51 @@ TEST(File, a_file_read_into_memory_keeps_its_name) {
"about.odt");
}

TEST(File, a_decoded_package_keeps_the_file_it_was_read_from) {
for (const FileType type : all_file_types()) {
if (!capabilities_by_file_type(type).create) {
continue;
}
std::ostringstream bytes;
create_document(type).save(bytes);
const std::string name = "blank." + file_type_to_string(type);
const std::size_t size = bytes.str().size();

for (const DecodedFile &decoded :
{open(File::from_memory(bytes.str(), name)),
open(File::from_memory(bytes.str(), name),
DecodeOptions::as(type))}) {
EXPECT_EQ(decoded.file().name(), name) << name;
EXPECT_EQ(decoded.file().size(), size) << name;
}
}
}

/// The engines that `create_document` cannot reach: ooxml in a compound file,
/// legacy Office and iWork.
TEST(File, a_decoded_file_on_disk_keeps_its_name) {
struct Case {
const char *path;
const char *name;
};
const std::array cases{
Case{"odr-public/docx/encrypted.docx", "encrypted.docx"},
Case{"odr-public/doc/file-sample_100kB.doc", "file-sample_100kB.doc"},
Case{"odr-public/pages/empty.pages", "empty.pages"},
};

for (const auto &[path, name] : cases) {
const DecodedFile decoded = open(TestData::test_file_path(path));
EXPECT_EQ(decoded.file().name(), name) << path;
EXPECT_EQ(open(TestData::test_file_path(path),
DecodeOptions::as(decoded.file_type()))
.file()
.name(),
name)
<< path;
}
}

TEST(File, copying_into_memory_uses_one_size_snapshot) {
class ChangingFile final : public internal::abstract::File {
public:
Expand Down
16 changes: 16 additions & 0 deletions wasm/tests/smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ describe('smoke', () => {
}
});

// A package reads through its archive, and still names the file it came from.
it('names a package upload, and leaves a new document unnamed', () => {
for (const [doc, name] of [
[odr.open(minimalOdt(), { name: 'letter.odt' }), 'letter.odt'],
[odr.open(minimalOdt()), ''],
[odr.create(odr.enums.FileType.odt), ''],
[odr.create(odr.enums.FileType.xlsx), ''],
]) {
try {
assert.equal(doc.fileName, name);
} finally {
doc.close();
}
}
});

it('throws a typed error for a wrong password', () => {
const doc = odr.open(fixture('encrypted.docx'));
try {
Expand Down
Loading