Skip to content

Conversation

@shrrrriya
Copy link

@shrrrriya shrrrriya commented Jan 24, 2026

Required for kiwix/kiwix-tools#794

This PR enhances Manager::addBookFromPathAndGetId() to signal failures
using specific exceptions instead of returning an empty string.

Changes:

  • Throw FileNotFound when the ZIM path cannot be read
  • Throw InvalidZim when required metadata is missing (when checkMetaData is enabled)
  • Throw LibraryNotWritable when adding the book to the library fails
  • Preserve existing behavior by inverting the validation logic (no functional change)

Tests:

  • Add unit tests covering detectable failure cases (missing file, invalid ZIM)

This change is required for kiwix-tools#794, which reports precise
user-facing errors by catching these exceptions.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes Manager::addBookFromPathAndGetId() to signal failures via typed exceptions (instead of returning an empty string), and adds unit tests for some failure scenarios.

Changes:

  • Introduces new exception types in include/kiwix/Error.h (e.g., FileNotFound, InvalidZim, LibraryNotWritable).
  • Updates Manager::addBookFromPathAndGetId() to throw exceptions for read/validation/add failures.
  • Adds new unit tests asserting exception behavior for missing files and “invalid” ZIM inputs.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
src/manager.cpp Throws typed exceptions from addBookFromPathAndGetId() instead of returning "".
include/kiwix/Error.h Adds new public exception classes intended for callers to catch.
test/manager.cpp Adds GTest cases expecting specific exceptions for failure paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

kiwix::InvalidZim
);

std::remove(fakeZim.c_str());
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::remove is used here but <cstdio> is not included in this file. Add the appropriate header (or switch to std::filesystem::remove if you prefer) to avoid relying on indirect includes.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +4
#pragma once

#include <exception>

Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This header is placed under include/kiwix/, but the Meson install list in include/meson.build currently installs only the top-level headers. As a result, <kiwix/Error.h> is likely not installed/packaged for downstream consumers. Update the build install rules to install this new public header (e.g. add it to include/meson.build or add an additional install_headers() call).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +6
#pragma once

#include <exception>

namespace kiwix {

Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Project public headers consistently use traditional include guards and the namespace kiwix\n{ style (e.g. include/manager.h, include/library.h). To match existing conventions, consider replacing #pragma once with the standard #ifndef/#define/#endif guard and aligning the namespace formatting.

Copilot uses AI. Check for mistakes.
Comment on lines +220 to 223
if (!this->readBookFromPath(pathToOpen, &book)) {
throw kiwix::FileNotFound();
}

Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readBookFromPath() returns false for any zim::Archive/book->update() exception (including invalid/corrupt ZIMs and permission errors), but this code always maps that to FileNotFound. This makes InvalidZim unreachable for many real invalid-ZIM cases and misclassifies permission problems. Consider checking fileExists/fileReadable first (and throwing FileNotFound/PermissionDenied accordingly), and treating archive parsing/metadata extraction failures as InvalidZim (either by letting readBookFromPath propagate/categorize exceptions or by adding a richer result).

Suggested change
if (!this->readBookFromPath(pathToOpen, &book)) {
throw kiwix::FileNotFound();
}
// Distinguish between missing files, permission issues, and invalid/corrupt ZIMs.
if (!fileExists(pathToOpen)) {
throw kiwix::FileNotFound();
}
if (!fileReadable(pathToOpen)) {
throw kiwix::PermissionDenied();
}
if (!this->readBookFromPath(pathToOpen, &book)) {
// The file exists and is readable, so a failure here most likely
// indicates an invalid or corrupt ZIM archive.
throw kiwix::InvalidZim();
}

Copilot uses AI. Check for mistakes.
Comment on lines +220 to +222
if (!this->readBookFromPath(pathToOpen, &book)) {
throw kiwix::FileNotFound();
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manager::addBookFromPath() currently delegates to addBookFromPathAndGetId() without catching exceptions. With the new throws here, the boolean wrapper will now throw instead of returning false, breaking its documented contract and existing call sites (e.g. tests/helpers that rely on a boolean). If addBookFromPath is intended to remain a non-throwing convenience API, it should catch these kiwix::* exceptions and return false.

Copilot uses AI. Check for mistakes.
Comment on lines +241 to 251
try {
manipulator.addBookToLibrary(book);
} catch (...) {
throw kiwix::LibraryNotWritable();
}

return book.getId();
}


/* Wrapper over Manager::addBookFromPath which return a bool instead of a string
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LibraryManipulator::addBookToLibrary() is a simple wrapper returning bool and does not throw in normal operation (see same file, LibraryManipulator::addBookToLibrary). Catching ... here is likely ineffective and can also hide unrelated exceptions (e.g. allocation errors) by rethrowing LibraryNotWritable. Prefer removing the catch, or catching only the specific exception type(s) that represent a write failure and preserving the original error context.

Suggested change
try {
manipulator.addBookToLibrary(book);
} catch (...) {
throw kiwix::LibraryNotWritable();
}
return book.getId();
}
/* Wrapper over Manager::addBookFromPath which return a bool instead of a string
manipulator.addBookToLibrary(book);
return book.getId();
}
/* Wrapper over Manager::addBookFromPath which return a bool instead of a string
}
/* Wrapper over Manager::addBookFromPath which return a bool instead of a string

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +126
EXPECT_THROW(
manager.addBookFromPathAndGetId(
"/this/path/does/not/exist.zim",
"",
"",
false
),
kiwix::FileNotFound
);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows, kiwix::isRelativePath() treats paths like "/this/path" as relative (see src/tools/pathTools.cpp:83-95), so this test may not be exercising the intended missing-file case consistently across platforms. Consider generating a guaranteed-nonexistent path under a temp directory (or under ./test/) and using that instead of a hard-coded POSIX-style absolute path.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +150
const std::string fakeZim = "not_a_real_zim.zim";

// Create a fake file that is NOT a valid ZIM
std::ofstream out(fakeZim);
out << "this is not a zim file";
out.close();

EXPECT_THROW(
manager.addBookFromPathAndGetId(
fakeZim,
"",
"",
true
),
kiwix::InvalidZim
);

std::remove(fakeZim.c_str());
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test creates a non-ZIM text file and expects InvalidZim, but addBookFromPathAndGetId() currently throws FileNotFound whenever readBookFromPath() fails (and readBookFromPath() returns false for invalid/corrupt ZIM parsing errors). As written, this is very likely to throw FileNotFound instead of InvalidZim. Either adjust the production code to classify parse failures as InvalidZim, or change the test to use a valid ZIM with missing required metadata (e.g. ./test/poor.zim) to exercise the metadata-validation path.

Suggested change
const std::string fakeZim = "not_a_real_zim.zim";
// Create a fake file that is NOT a valid ZIM
std::ofstream out(fakeZim);
out << "this is not a zim file";
out.close();
EXPECT_THROW(
manager.addBookFromPathAndGetId(
fakeZim,
"",
"",
true
),
kiwix::InvalidZim
);
std::remove(fakeZim.c_str());
// Use a ZIM file that exists but is invalid due to missing/invalid metadata.
const std::string invalidZimPath = "./test/poor.zim";
EXPECT_THROW(
manager.addBookFromPathAndGetId(
invalidZimPath,
"",
"",
true
),
kiwix::InvalidZim
);

Copilot uses AI. Check for mistakes.

namespace kiwix {

class KiwixError : public std::exception {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::runtime_error may be a more proper base type

Comment on lines +16 to +18
const char* what() const noexcept override {
return "ZIM file not found";
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't restrict this error to ZIM-files. Also an error message should be more informative, in this case knowing the path of the file in question would help (this applies to other errors in this commit too).

Comment on lines +21 to +26
class PermissionDenied : public KiwixError {
public:
const char* what() const noexcept override {
return "Permission denied while accessing ZIM file";
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be renamed to FileNotReadableError (and in general, all error classes should have Error in their name).

Comment on lines +35 to +40
class LibraryNotWritable : public KiwixError {
public:
const char* what() const noexcept override {
return "Library file is not writable";
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename this to FileNotWritableError and change the error message accordingly (so that it includes the path of the file in question).

Comment on lines 245 to 249
try {
manipulator.addBookToLibrary(book);
} catch (...) {
throw kiwix::LibraryNotWritable();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can we be sure that manipulator.addBookToLibrary() throws only if the library is not writable? We don't need this try/catch block. LibraryManipulator::addBookToLibrary() (or any other functions it calls) should throw directly the right type of exception for each error situation.

Comment on lines +128 to +148
TEST(ManagerTest, addBookFromPathThrowsOnInvalidZim)
{
auto lib = kiwix::Library::create();
kiwix::Manager manager(lib);

const std::string fakeZim = "not_a_real_zim.zim";

// Create a fake file that is NOT a valid ZIM
std::ofstream out(fakeZim);
out << "this is not a zim file";
out.close();

EXPECT_THROW(
manager.addBookFromPathAndGetId(
fakeZim,
"",
"",
true
),
kiwix::InvalidZim
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you run this test? I don't think that it should pass.

Comment on lines +113 to +151
TEST(ManagerTest, addBookFromPathThrowsOnMissingFile)
{
auto lib = kiwix::Library::create();
kiwix::Manager manager(lib);

EXPECT_THROW(
manager.addBookFromPathAndGetId(
"/this/path/does/not/exist.zim",
"",
"",
false
),
kiwix::FileNotFound
);
}
TEST(ManagerTest, addBookFromPathThrowsOnInvalidZim)
{
auto lib = kiwix::Library::create();
kiwix::Manager manager(lib);

const std::string fakeZim = "not_a_real_zim.zim";

// Create a fake file that is NOT a valid ZIM
std::ofstream out(fakeZim);
out << "this is not a zim file";
out.close();

EXPECT_THROW(
manager.addBookFromPathAndGetId(
fakeZim,
"",
"",
true
),
kiwix::InvalidZim
);

std::remove(fakeZim.c_str());
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All error situations should be tested. If they are difficult to test, it's OK to start with a limited number of error situations that we detect and add other error types later. All code that is written should be covered by tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants