diff --git a/CMakeRC.cmake b/CMakeRC.cmake index 6a5147a..65fb588 100644 --- a/CMakeRC.cmake +++ b/CMakeRC.cmake @@ -340,7 +340,7 @@ public: : _index(&index) {} - file open(const std::string& path) const { + const detail::file_data& _get_file_data(const std::string& path) const { auto entry_ptr = _get(path); if (!entry_ptr || !entry_ptr->is_file()) { #ifdef CMRC_NO_EXCEPTIONS @@ -350,10 +350,46 @@ public: throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), path); #endif } - auto& dat = entry_ptr->as_file(); + return entry_ptr->as_file(); + } + + file open(const std::string& path) const { + auto& dat = _get_file_data(path); return file{dat.begin_ptr, dat.end_ptr}; } + std::string get_as_string(const std::string& path) const { + auto& dat = _get_file_data(path); + auto f = file{dat.begin_ptr, dat.end_ptr}; + return std::string{dat.begin_ptr, f.size()}; + } + +#if __cplusplus >= 201703L + std::string_view get_as_string_view(const std::string& path) const { + auto& dat = _get_file_data(path); + auto f = file{dat.begin_ptr, dat.end_ptr}; + return std::string_view{dat.begin_ptr, f.size()}; + } +#endif + + const char* get_as_raw_ptr(const std::string& path, std::size_t& file_size) const { + auto& dat = _get_file_data(path); + auto f = file{dat.begin_ptr, dat.end_ptr}; + file_size = f.size(); + return dat.begin_ptr; + } + + const char* get_as_raw_ptr(const std::string& path) const { + std::size_t sz; + return get_as_raw_ptr(path, sz); + } + + std::size_t get_size(const std::string& path) const { + auto& dat = _get_file_data(path); + auto f = file{dat.begin_ptr, dat.end_ptr}; + return f.size(); + } + bool is_file(const std::string& path) const noexcept { auto entry_ptr = _get(path); return entry_ptr && entry_ptr->is_file(); diff --git a/README.md b/README.md index 5672799..3c43c6d 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,15 @@ need. For [vcpkg](https://github.com/microsoft/vcpkg) users there is a `cmakerc` [port](https://github.com/microsoft/vcpkg/tree/master/ports/cmakerc) that can be installed via `vcpkg install cmakerc` or by adding it to `dependencies` section of your `vcpkg.json` file. +The following sections will demonstrate how to use CMRC. + +A complete example is available at the very end of the documentation if you are +impatient or want to take a sneak peek. + ## Usage +The following steps outline how to use CMRC. + 1. Once installed, simply import the `CMakeRC.cmake` script. If you placed the module in your project directory (recommended), simply use `include(CMakeRC)` to import the module. If you installed it as a package, use `find_package(CMakeRC)`. @@ -57,20 +64,27 @@ For [vcpkg](https://github.com/microsoft/vcpkg) users there is a `cmakerc` [port Where `...` is simply a list of files that you wish to compile into the resource library. - You can use the `ALIAS` argument to immediately generate an alias target for - the resource library (recommended): + You can use the `ALIAS` argument to immediately generate an alias target for + the resource library (recommended): - ```cmake - cmrc_add_resource_library(foo-resources ALIAS foo::rc ...) - ``` + ```cmake + cmrc_add_resource_library(foo-resources ALIAS foo::rc ...) + ``` - **Note:** If the name of the library target is not a valid C++ `namespace` - identifier, you will need to provide the `NAMESPACE` argument. Otherwise, the - name of the library will be used as the resource library's namespace. + **Note:** If the name of the library target is not a valid C++ `namespace` + identifier, you will need to provide the `NAMESPACE` argument. Otherwise, the + name of the library will be used as the resource library's namespace. + + ```cmake + cmrc_add_resource_library(foo-resources ALIAS foo::rc NAMESPACE foo ...) + ``` + + When the library is created you can at any time add additional resources: + + ```cmake + cmrc_add_resources(foo-resources ...) + ``` - ```cmake - cmrc_add_resource_library(foo-resources ALIAS foo::rc NAMESPACE foo ...) - ``` 3. To use the resource library, link the resource library target into a binary using `target_link_libraries()`: @@ -133,18 +147,46 @@ statically allocated resource library data. ### Methods on `cmrc::embedded_filesystem` -- `open(const std::string& path) -> cmrc::file` - Opens and returns a - non-directory `file` object at `path`, or throws `std::system_error()` on - error. +Filesystem inspection: + - `is_file(const std::string& path) -> bool` - Returns `true` if the given - `path` names a regular file, `false` otherwise. + `path` names a regular file, `false` otherwise - `is_directory(const std::string& path) -> bool` - Returns `true` if the given - `path` names a directory. `false` otherwise. -- `exists(const std::string& path) -> bool` returns `true` if the given path - names an existing file or directory, `false` otherwise. -- `iterate_directory(const std::string& path) -> cmrc::directory_iterator` - returns a directory iterator for iterating the contents of a directory. Throws - if the given `path` does not identify a directory. + `path` names a directory. `false` otherwise +- `exists(const std::string& path) -> bool` - Returns `true` if the given path + names an existing file or directory, `false` otherwise +- `iterate_directory(const std::string& path) -> cmrc::directory_iterator` - + Returns a directory iterator for iterating the contents of a directory, or + throws if the given `path` does not identify a directory +- `get_size(const std::string& path) -> std::size_t` - Returns the size in bytes + of the file `path`, or throw `std::system_error()` on error + +File access (these will throw `std::system_error()` if used on a directory): + +- `open(const std::string& path) -> cmrc::file` - Opens `path` and returns a + `file` object from where data can be read +- `get_as_string(const std::string& path) -> std::string` - Returns a copy of + the data for `path` as a `std::string` +- `get_as_string_view(const std::string& path) -> std::string_view` - Returns + the data for `path` as a `std::string_view` (no deep copy, requires C++17) +- `get_as_raw_ptr(const std::string& path) -> const char*` - + Returns a raw `const char*` pointer to the the non-directory data at `path` +- `get_as_raw_ptr(const std::string& path, std::size_t& file_size) -> const char*` - + Returns a raw `const char*` pointer to the non-directory data at `path` and + updates the supplied `file_size` reference with the data size + +If you want to use `get_as_string_view()` you need a C++17 (or newer) compliant +compiler. Example CMake configuration for this: + +```cmake +target_compile_features(your_target PRIVATE cxx_std_17) +if(MSVC) + # MSVC favors their own backwards compatibilty over standards compliance. + # The below option ensures the __cplusplus pre-processing variable in MSVC + # is actually standards compliant. + target_compile_options(your_target PRIVATE "/Zc:__cplusplus") +endif() +``` ## Members of `cmrc::file` @@ -179,6 +221,10 @@ the library using `cmrc_add_resources` with the name of the library and the paths to any additional resources that you wish to compile in. This way you can lazily add resources to the library as your configure script runs. +Resources are always stored with an extra trailing `null` byte to facilitate +the direct use of data as C strings if needed. The `null` byte is not a part of +the actual data and does not count towards its size. + Both `cmrc_add_resource_library` and `cmrc_add_resources` take two additional keyword parameters: @@ -219,9 +265,131 @@ cmrc_add_resource_library( ) ``` +This will result in the following files in the resource file system: + +``` +flowers/rose.jpg +flowers/tulip.jpg +flowers/daisy.jpg +flowers/sunflower.jpg +``` + +## A complete example + +This complete example shows how to use the library and the different ways +resources can be accessed. Only text file resources are used in this +example, but binary data is of course supported using the same interface. + +**Files needed:** +```cmake +CMakeLists.txt +example.cpp +CMakeRC.cmake # Module file copied from the CMRC project +LICENSE.txt # Added resource +README.txt # Added resource +``` + +**CMakeLists.txt** + +```cmake +# Require a decent version of CMake +cmake_minimum_required(VERSION 3.6) + +# Include the CMRC module +include(./CMakeRC.cmake) + +# Define the example project +project(cmrc_example) + +# Add an executable to the project +add_executable(cmrc_example example.cpp) + +# In this case we would like to use get_as_string_view() which requries C++17. +# If this is not needed the following lines can be skipped. +target_compile_features(cmrc_example PRIVATE cxx_std_17) +if(MSVC) + # MSVC favors their own backwards compatibilty over standards compliance. + # The below option ensures the __cplusplus pre-processing variable in MSVC + # is actually standards compliant. + target_compile_options(cmrc_example PRIVATE "/Zc:__cplusplus") +endif() + +# Create a "resource library" called "myrclib" which will hold the resources. +# We also create a CMake ALIAS to the library which can be used when linking. +# Finally we choose to add a text file as a first resource. +cmrc_add_resource_library( + myrclib + NAMESPACE rc + ALIAS cmrc_example::rc + + LICENSE.txt +) + +# Add more resource files to the library +cmrc_add_resources(myrclib README.txt) + +# Add the resource library using the library ALIAS +target_link_libraries(cmrc_example PRIVATE cmrc_example::rc) + +# ... or using the library name if preferred +#target_link_libraries(cmrc_example PRIVATE myrclib) +``` + +**example.cpp** + ```c++ -int foo() { - auto fs = cmrc::flower::get_filesystem(); - auto rose = fs.open("flowers/rose.jpg"); +#include +#include + +// Declare the resource library. Use the NAMESPACE specified in CMakeFile.txt. +CMRC_DECLARE(rc); + +// Small helper to make output tidy +void header(const char *s) { + std::cout << "\n\n---===[ " << s << " ]===---\n\n"; +} + +int main() { + // Get the filesystem object from the specified namespace (under cmrc::) + auto fs = cmrc::rc::get_filesystem(); + + // Access the text file resource in all possible ways and display the contents each time + + header("open() returning iterable cmrc::file object"); + auto license_file = fs.open("LICENSE.txt"); + for(auto i = license_file.begin(); i != license_file.end(); ++i) { + std::cout << *i; + } + std::cout << "\n"; + + header("String (std::string), will return a copy of the data"); + auto license_string= fs.get_as_string("LICENSE.txt"); + std::cout << license_string << "\n"; + + header("String view (std::string_view), read-only view, avoids a copy of the data"); + auto license_string_view = fs.get_as_string_view("LICENSE.txt"); + std::cout << license_string_view << "\n"; + + header("String view (std::string_view) again, accessed using a raw pointer"); + auto license_string_view2 = fs.get_as_string_view("LICENSE.txt"); + auto license_string_view2_data = license_string_view2.data(); // const char* + auto license_string_view2_size = license_string_view2.size(); // std::size_t + std::cout << std::string(license_string_view2_data, license_string_view2_size) << "\n"; + + header("Raw pointer (const char*), relying on CMRC implicit trailing NULL byte"); + auto license_raw_ptr = fs.get_as_raw_ptr("LICENSE.txt"); + std::cout << std::string(license_raw_ptr) << "\n"; // Trailing NULL byte needed! + + header("Raw pointer (const char*) with size return argument"); + std::size_t license_raw_ptr2_size; + auto license_raw_ptr2 = fs.get_as_raw_ptr("LICENSE.txt", license_raw_ptr2_size); + std::cout << std::string(license_raw_ptr2, license_raw_ptr2_size) << "\n"; + + header("Raw pointer (const char*) and size (std::size_t)"); + auto license_raw_ptr3 = fs.get_as_raw_ptr("LICENSE.txt"); + auto license_raw_ptr3_size = fs.get_size("LICENSE.txt"); + std::cout << std::string(license_raw_ptr3, license_raw_ptr3_size) << "\n"; + + return 0; } ``` diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 0000000..d3eba77 --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,41 @@ +# Require a decent version of CMake +cmake_minimum_required(VERSION 3.6) + +# Include the CMRC module +include(../CMakeRC.cmake) + +# Define the example project +project(cmrc_example) + +# Add an executable to the project +add_executable(cmrc_example example.cpp) + +# In this case we would like to use get_as_string_view() which requries C++17. +# If this is not needed the following lines can be skipped. +target_compile_features(cmrc_example PRIVATE cxx_std_17) +if(MSVC) + # MSVC favors their own backwards compatibilty over standards compliance. + # The below option ensures the __cplusplus pre-processing variable in MSVC + # is actually standards compliant. + target_compile_options(cmrc_example PRIVATE "/Zc:__cplusplus") +endif() + +# Create a "resource library" called "myrclib" which will hold the resources. +# We also create a CMake ALIAS to the library which can be used when linking. +# Finally we choose to add a text file as a first resource. +cmrc_add_resource_library( + myrclib + NAMESPACE rc + ALIAS cmrc_example::rc + + LICENSE.txt +) + +# Add more resource files to the library +cmrc_add_resources(myrclib README.txt) + +# Add the resource library using the library ALIAS +target_link_libraries(cmrc_example PRIVATE cmrc_example::rc) + +# ... or using the library name if preferred +#target_link_libraries(cmrc_example PRIVATE myrclib) diff --git a/example/LICENSE.txt b/example/LICENSE.txt new file mode 100644 index 0000000..10cb45d --- /dev/null +++ b/example/LICENSE.txt @@ -0,0 +1,16 @@ +MIT No Attribution + +Copyright 2023 technoyes + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/example/README.txt b/example/README.txt new file mode 100644 index 0000000..9ec788e --- /dev/null +++ b/example/README.txt @@ -0,0 +1 @@ +The files in this directory are available under the MIT No Attribution License. diff --git a/example/example.cpp b/example/example.cpp new file mode 100644 index 0000000..c8f672e --- /dev/null +++ b/example/example.cpp @@ -0,0 +1,54 @@ +#include +#include + +// Declare the resource library. Use the NAMESPACE specified in CMakeFile.txt. +CMRC_DECLARE(rc); + +// Small helper to make output tidy +void header(const char *s) { + std::cout << "\n\n---===[ " << s << " ]===---\n\n"; +} + +int main() { + // Get the filesystem object from the specified namespace (under cmrc::) + auto fs = cmrc::rc::get_filesystem(); + + // Access the text file resource in all possible ways and display the contents each time + + header("open() returning iterable cmrc::file object"); + auto license_file = fs.open("LICENSE.txt"); + for(auto i = license_file.begin(); i != license_file.end(); ++i) { + std::cout << *i; + } + std::cout << "\n"; + + header("String (std::string), will return a copy of the data"); + auto license_string= fs.get_as_string("LICENSE.txt"); + std::cout << license_string << "\n"; + + header("String view (std::string_view), read-only view, avoids a copy of the data"); + auto license_string_view = fs.get_as_string_view("LICENSE.txt"); + std::cout << license_string_view << "\n"; + + header("String view (std::string_view) again, accessed using a raw pointer"); + auto license_string_view2 = fs.get_as_string_view("LICENSE.txt"); + auto license_string_view2_data = license_string_view2.data(); // const char* + auto license_string_view2_size = license_string_view2.size(); // std::size_t + std::cout << std::string(license_string_view2_data, license_string_view2_size) << "\n"; + + header("Raw pointer (const char*), relying on CMRC implicit trailing NULL byte"); + auto license_raw_ptr = fs.get_as_raw_ptr("LICENSE.txt"); + std::cout << std::string(license_raw_ptr) << "\n"; // Trailing NULL byte needed! + + header("Raw pointer (const char*) with size return argument"); + std::size_t license_raw_ptr2_size; + auto license_raw_ptr2 = fs.get_as_raw_ptr("LICENSE.txt", license_raw_ptr2_size); + std::cout << std::string(license_raw_ptr2, license_raw_ptr2_size) << "\n"; + + header("Raw pointer (const char*) and size (std::size_t)"); + auto license_raw_ptr3 = fs.get_as_raw_ptr("LICENSE.txt"); + auto license_raw_ptr3_size = fs.get_size("LICENSE.txt"); + std::cout << std::string(license_raw_ptr3, license_raw_ptr3_size) << "\n"; + + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1a55c79..4382dbe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -38,6 +38,32 @@ cmrc_add_test( subdir_a/subdir_b/file_b.txt ) +cmrc_add_test( + NAME simple_string + PASS_REGEX "^Hello, world!" + RESOURCES + hello.txt + ) + +cmrc_add_test( + NAME simple_string_view + PASS_REGEX "^Hello, world!" + RESOURCES + hello.txt + ) +target_compile_features(simple_string_view PRIVATE cxx_std_17) +if(MSVC) + # MSVC favors their own backwards compatibilty over standards compliance + target_compile_options(simple_string_view PRIVATE "/Zc:__cplusplus") +endif() + +cmrc_add_test( + NAME simple_raw_ptr + PASS_REGEX "^Hello, world!" + RESOURCES + hello.txt + ) + cmrc_add_test( NAME flower RESOURCES flower.jpg @@ -74,6 +100,17 @@ cmrc_add_test( subdir_a/subdir_b/file_b.txt ) +cmrc_add_test( + NAME fileaccess + RESOURCES flower.jpg + TEST_ARGV "${CMAKE_CURRENT_SOURCE_DIR}/flower.jpg" + ) +target_compile_features(fileaccess PRIVATE cxx_std_17) +if(MSVC) + # MSVC favors their own backwards compatibilty over standards compliance + target_compile_options(fileaccess PRIVATE "/Zc:__cplusplus") +endif() + cmrc_add_test( NAME enoent WILL_FAIL diff --git a/tests/fileaccess.cpp b/tests/fileaccess.cpp new file mode 100644 index 0000000..e33aeec --- /dev/null +++ b/tests/fileaccess.cpp @@ -0,0 +1,101 @@ +#include + +#include +#include +#include +#include + +CMRC_DECLARE(fileaccess); + +int main(int argc, char** argv) { + // Determine size of file on actual disk + if (argc != 2) { + std::cerr << "Invalid arguments passed to fileaccess\n"; + return 2; + } + std::cout << "Reading flower from " << argv[1] << '\n'; + std::ifstream flower_fs{argv[1], std::ios_base::binary}; + if (!flower_fs) { + std::cerr << "Invalid filename passed to fileaccess: " << argv[1] << '\n'; + return 2; + } + using iter = std::istreambuf_iterator; + const auto fs_size = std::distance(iter(flower_fs), iter()); + flower_fs.seekg(0); + std::vector flower_fs_vector((iter(flower_fs)), iter()); + flower_fs.seekg(0); + + // Get handle to resource filesystem + auto fs = cmrc::fileaccess::get_filesystem(); + + // Test access using open() + auto flower_open = fs.open("flower.jpg"); + const auto open_size = std::distance(flower_open.begin(), flower_open.end()); + if (fs_size != open_size) { + std::cerr << "open(): Sizes do not match: FS == " << fs_size << ", RC == " << open_size + << "\n"; + return 1; + } + if (!std::equal(flower_open.begin(), flower_open.end(), iter(flower_fs))) { + std::cerr << "open(): Contents do not match\n"; + return 1; + } + flower_fs.seekg(0); + + // Test access using get_as_string() + auto flower_string = fs.get_as_string("flower.jpg"); + auto flower_string_size = flower_string.size(); + if(fs_size != flower_string_size) { + std::cerr << "get_as_string(): Sizes do not match: FS == " << fs_size << ", RC == " + << flower_string_size << "\n"; + return 1; + } + if(0 != memcmp(flower_fs_vector.data(), flower_string.data(), flower_string_size)) { + std::cerr << "get_as_string(): Contents do not match\n"; + return 1; + } + +#if __cplusplus >= 201703L + // Test access using get_as_string_view() + auto flower_string_view = fs.get_as_string_view("flower.jpg"); + auto flower_string_view_size = flower_string_view.size(); + if(fs_size != flower_string_view_size) { + std::cerr << "get_as_string_view(): Sizes do not match: FS == " << fs_size << ", RC == " + << flower_string_view_size << "\n"; + return 1; + } + if(0 != memcmp(flower_fs_vector.data(), flower_string_view.data(), flower_string_view_size)) { + std::cerr << "get_as_string_view(): Contents do not match\n"; + return 1; + } +#endif + + // Test access using get_as_raw_ptr() + get_size() + auto flower_raw_ptr = fs.get_as_raw_ptr("flower.jpg"); + auto flower_raw_ptr_size = fs.get_size("flower.jpg"); + if(fs_size != flower_raw_ptr_size) { + std::cerr << "get_as_raw_ptr()+get_size(): Sizes do not match: FS == " << fs_size << ", RC == " + << flower_raw_ptr_size << "\n"; + return 1; + } + if(0 != memcmp(flower_fs_vector.data(), flower_raw_ptr, flower_raw_ptr_size)) { + std::cerr << "get_as_raw_ptr()+get_size(): Contents do not match\n"; + return 1; + } + + // Test access using get_as_raw_ptr() + std::size_t flower_raw_ptr_size2{0}; + auto flower_raw_ptr2 = fs.get_as_raw_ptr("flower.jpg", flower_raw_ptr_size2); + if(fs_size != flower_raw_ptr_size2) { + std::cerr << "get_as_raw_ptr(): Sizes do not match: FS == " << fs_size << ", RC == " + << flower_raw_ptr_size2 << "\n"; + return 1; + } + if(0 != memcmp(flower_fs_vector.data(), flower_raw_ptr2, flower_raw_ptr_size2)) { + std::cerr << "get_as_raw_ptr(): Contents do not match\n"; + return 1; + } + + // Explictly return success + return 0; +} \ No newline at end of file diff --git a/tests/flower.cpp b/tests/flower.cpp index aa845c2..be7dd78 100644 --- a/tests/flower.cpp +++ b/tests/flower.cpp @@ -35,4 +35,5 @@ int main(int argc, char** argv) { std::cerr << "Flower file contents do not match\n"; return 1; } -} \ No newline at end of file + return 0; +} diff --git a/tests/simple_raw_ptr.cpp b/tests/simple_raw_ptr.cpp new file mode 100644 index 0000000..3622a97 --- /dev/null +++ b/tests/simple_raw_ptr.cpp @@ -0,0 +1,13 @@ +#include + +#include + +CMRC_DECLARE(simple_raw_ptr); + +int main() { + auto fs = cmrc::simple_raw_ptr::get_filesystem(); + const char* data = fs.get_as_raw_ptr("hello.txt"); + // This kind of use relies on the implicit NULL byte added by + // CMRC as well as that the file has no NULL bytes inside it. + std::cout << std::string(data) << '\n'; +} diff --git a/tests/simple_string.cpp b/tests/simple_string.cpp new file mode 100644 index 0000000..b2d1f23 --- /dev/null +++ b/tests/simple_string.cpp @@ -0,0 +1,11 @@ +#include + +#include + +CMRC_DECLARE(simple_string); + +int main() { + auto fs = cmrc::simple_string::get_filesystem(); + std::string a_string = fs.get_as_string("hello.txt"); + std::cout << a_string << '\n'; +} diff --git a/tests/simple_string_view.cpp b/tests/simple_string_view.cpp new file mode 100644 index 0000000..6dc33e6 --- /dev/null +++ b/tests/simple_string_view.cpp @@ -0,0 +1,11 @@ +#include + +#include + +CMRC_DECLARE(simple_string_view); + +int main() { + auto fs = cmrc::simple_string_view::get_filesystem(); + std::string_view a_string_view = fs.get_as_string_view("hello.txt"); + std::cout << a_string_view << '\n'; +} \ No newline at end of file