Throw specific exceptions from Manager::addBookFromPathAndGetId - #1267
Throw specific exceptions from Manager::addBookFromPathAndGetId#1267shrrrriya wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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> | ||
|
|
There was a problem hiding this comment.
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 { | ||
|
|
There was a problem hiding this comment.
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(); | ||
| } | ||
|
|
There was a problem hiding this comment.
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(); | ||
| } |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 | ||
| ); |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
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.
|
@shrrrriya Any feedback on this PR? we are about to make a release soon, would be a good time to complete your effort! |
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.