-
-
Notifications
You must be signed in to change notification settings - Fork 78
Throw specific exceptions from Manager::addBookFromPathAndGetId #1267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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()); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
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.
| #pragma once | ||
|
|
||
| #include <exception> | ||
|
|
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
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).
| #pragma once | ||
|
|
||
| #include <exception> | ||
|
|
||
| namespace kiwix { | ||
|
|
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
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.
| if (!this->readBookFromPath(pathToOpen, &book)) { | ||
| throw kiwix::FileNotFound(); | ||
| } | ||
|
|
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
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).
| 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(); | |
| } |
| if (!this->readBookFromPath(pathToOpen, &book)) { | ||
| throw kiwix::FileNotFound(); | ||
| } |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
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.
| try { | ||
| manipulator.addBookToLibrary(book); | ||
| } catch (...) { | ||
| throw kiwix::LibraryNotWritable(); | ||
| } | ||
|
|
||
| return book.getId(); | ||
| } | ||
|
|
||
|
|
||
| /* Wrapper over Manager::addBookFromPath which return a bool instead of a string |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
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.
| 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 |
| EXPECT_THROW( | ||
| manager.addBookFromPathAndGetId( | ||
| "/this/path/does/not/exist.zim", | ||
| "", | ||
| "", | ||
| false | ||
| ), | ||
| kiwix::FileNotFound | ||
| ); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
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.
| 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()); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
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.
| 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 | |
| ); |
|
|
||
| namespace kiwix { | ||
|
|
||
| class KiwixError : public std::exception { |
There was a problem hiding this comment.
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
| const char* what() const noexcept override { | ||
| return "ZIM file not found"; | ||
| } |
There was a problem hiding this comment.
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).
| class PermissionDenied : public KiwixError { | ||
| public: | ||
| const char* what() const noexcept override { | ||
| return "Permission denied while accessing ZIM file"; | ||
| } | ||
| }; |
There was a problem hiding this comment.
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).
| class LibraryNotWritable : public KiwixError { | ||
| public: | ||
| const char* what() const noexcept override { | ||
| return "Library file is not writable"; | ||
| } | ||
| }; |
There was a problem hiding this comment.
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).
| try { | ||
| manipulator.addBookToLibrary(book); | ||
| } catch (...) { | ||
| throw kiwix::LibraryNotWritable(); | ||
| } |
There was a problem hiding this comment.
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.
| 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 | ||
| ); |
There was a problem hiding this comment.
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.
| 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()); | ||
| } |
There was a problem hiding this comment.
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.
Required for kiwix/kiwix-tools#794
This PR enhances Manager::addBookFromPathAndGetId() to signal failures
using specific exceptions instead of returning an empty string.
Changes:
Tests:
This change is required for kiwix-tools#794, which reports precise
user-facing errors by catching these exceptions.