diff --git a/TODO.md b/TODO.md index a629122..6a6edfb 100644 --- a/TODO.md +++ b/TODO.md @@ -10,6 +10,8 @@ Not all of these are necessarily going to be implemented at all. * Merge wifissid.txt and wifipass.txt into wificreds.txt and merge mqttuser.txt and mqttpass.txt into mqttcreds.txt * Improve log messages for when measurements fail * Use an asynchronous DHT library(for example https://github.com/bertmelis/esp32DHT/)? + * Add (stream?) compression to uzlib_gzip_wrapper + * Change callback based uzlib_ungzip_wrapper to use C++ function objects instead of C function pointers ## Web Interface * Add error message to web interface if measurement fails @@ -19,6 +21,7 @@ Not all of these are necessarily going to be implemented at all. * Allow caching of static resources using a hash of their content as an ETag * Add theme switcher to web interface(clientside setting) * Web server IPv6 support + * Add current commit to prometheus info and HTTP Server header ## Integration * Find a way to make the kubernetes service(for prometheus) automatically point to the esp(mDNS?) @@ -28,3 +31,5 @@ Not all of these are necessarily going to be implemented at all. * Add MQTT state json * Cleanup MQTT code * Add MQTT discovery support(https://www.home-assistant.io/docs/mqtt/discovery/) + * Add WiFi info to prometheus metrics + * Dynamically gzip compress metrics page? diff --git a/lib/README b/lib/README deleted file mode 100644 index 6debab1..0000000 --- a/lib/README +++ /dev/null @@ -1,46 +0,0 @@ - -This directory is intended for project specific (private) libraries. -PlatformIO will compile them to static libraries and link into executable file. - -The source code of each library should be placed in a an own separate directory -("lib/your_library_name/[here are source files]"). - -For example, see a structure of the following two libraries `Foo` and `Bar`: - -|--lib -| | -| |--Bar -| | |--docs -| | |--examples -| | |--src -| | |- Bar.c -| | |- Bar.h -| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html -| | -| |--Foo -| | |- Foo.c -| | |- Foo.h -| | -| |- README --> THIS FILE -| -|- platformio.ini -|--src - |- main.c - -and a contents of `src/main.c`: -``` -#include -#include - -int main (void) -{ - ... -} - -``` - -PlatformIO Library Dependency Finder will find automatically dependent -libraries scanning project source files. - -More information about PlatformIO Library Dependency Finder -- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/fallback_log/README.md b/lib/fallback_log/README.md new file mode 100644 index 0000000..c3fd1b5 --- /dev/null +++ b/lib/fallback_log/README.md @@ -0,0 +1,2 @@ +# Fallback Log +This is a simple header-only library, containing fallback logging(log_X) macros to use if the framework for the given board doesn't include them. diff --git a/src/fallback_log.h b/lib/fallback_log/include/fallback_log.h similarity index 100% rename from src/fallback_log.h rename to lib/fallback_log/include/fallback_log.h diff --git a/lib/fallback_log/library.json b/lib/fallback_log/library.json new file mode 100644 index 0000000..1ae7892 --- /dev/null +++ b/lib/fallback_log/library.json @@ -0,0 +1,12 @@ +{ + "name": "FallbackLog", + "description": "A header only library containing fallback definitions of the log_X macros.", + "version": "1.0.0", + "license": "MIT", + "dependencies": [ + { + "name": "utils", + "version": "utils" + } + ] +} diff --git a/lib/utils/README.md b/lib/utils/README.md new file mode 100644 index 0000000..ac71373 --- /dev/null +++ b/lib/utils/README.md @@ -0,0 +1,2 @@ +# Utils +This library contains common utilities required for other parts of this project. diff --git a/src/utils.h b/lib/utils/include/utils.h similarity index 93% rename from src/utils.h rename to lib/utils/include/utils.h index 3799c5a..3756a0b 100644 --- a/src/utils.h +++ b/lib/utils/include/utils.h @@ -15,7 +15,7 @@ #include -namespace utility { +namespace utils { /** * A method to get the index of the Most Significant Bit set in a number. * @@ -58,12 +58,12 @@ struct const_expr_value { constexpr size_t strlen(const char *str) { return (*str == 0) ? 0 : strlen(str + 1) + 1; } -} +} /* namespace utils */ /** * CONST_EXPR_VALUE is an utility macro to get force compile time evaluation of a constexpr value. */ -#define CONST_EXPR_VALUE(exp) utility::const_expr_value::value +#define CONST_EXPR_VALUE(exp) utils::const_expr_value::value /** * FILE_BASE_NAME is a macro that represents the base name of the current file. @@ -71,7 +71,7 @@ constexpr size_t strlen(const char *str) { #ifdef __FILE_NAME__ #define FILE_BASE_NAME __FILE_NAME__ #else -#define FILE_BASE_NAME &__FILE__[CONST_EXPR_VALUE(utility::get_base_name_offset(__FILE__))] +#define FILE_BASE_NAME &__FILE__[CONST_EXPR_VALUE(utils::get_base_name_offset(__FILE__))] #endif /** @@ -79,7 +79,7 @@ constexpr size_t strlen(const char *str) { */ #define EXPAND_MACRO(macro) #macro -namespace utility { +namespace utils { /** * Converts the given temperature from degrees celsius to degrees fahrenheit. * @@ -115,6 +115,6 @@ std::string float_to_string(const float measurement, * @return The newly created string. */ std::string timespan_to_string(const int64_t time_ms); -} +} /* namespace utils */ #endif /* SRC_UTILS_H_ */ diff --git a/src/utils.cpp b/lib/utils/src/utils.cpp similarity index 97% rename from src/utils.cpp rename to lib/utils/src/utils.cpp index 2711af9..b83b331 100644 --- a/src/utils.cpp +++ b/lib/utils/src/utils.cpp @@ -13,7 +13,7 @@ #include #include -namespace utility { +namespace utils { uint8_t get_msb(const uint32_t number) { uint8_t idx = 0; @@ -65,4 +65,4 @@ std::string timespan_to_string(const int64_t time_ms) { return stream.str(); } -} /* namespace utility */ +} /* namespace utils */ diff --git a/lib/uzlib_gzip_wrapper/README.md b/lib/uzlib_gzip_wrapper/README.md new file mode 100644 index 0000000..251f04f --- /dev/null +++ b/lib/uzlib_gzip_wrapper/README.md @@ -0,0 +1,10 @@ +# UZLib GZIP Wrapper +This is a [UZLib](https://github.com/pfalcon/uzlib.git) wrapper specifically used to (de)compress GZIP files. + +**IMPORTANT:** The current version of this library cannot handle compression yet. + +This wrapper can handle both decompressing a file from a constant byte array, as well as from a callback. + +This wrapper can handle a custom window size. + +There are no usage examples at this point in time. diff --git a/lib/uzlib_gzip_wrapper/include/uzlib_gzip_wrapper.h b/lib/uzlib_gzip_wrapper/include/uzlib_gzip_wrapper.h new file mode 100644 index 0000000..24a3c35 --- /dev/null +++ b/lib/uzlib_gzip_wrapper/include/uzlib_gzip_wrapper.h @@ -0,0 +1,141 @@ +/* + * uzlib_gzip_wrapper.h + * + * Created on: May 26, 2023 + * + * Copyright (C) 2023 ToMe25. + * This project is licensed under the MIT License. + * The MIT license can be found in the project root and at https://opensource.org/licenses/MIT. + */ + +#ifndef SRC_HTML_UZLIB_GZIP_WRAPPER_H_ +#define SRC_HTML_UZLIB_GZIP_WRAPPER_H_ + +#include +#include + +namespace gzip { + +/** + * Initializes the static variables used by uzlib. + */ +void init(); + +/** + * A wrapper to help with storing the data associated with decompressing a gzip file using uzlib. + * Can currently only handle the entire compressed file being accessible as a single pointer block. + */ +class uzlib_ungzip_wrapper { +private: + /** + * The internal data store used by uzlib. + */ + uzlib_uncomp *decomp; + + /** + * The number of already decompressed bytes. + */ + size_t index = 0; + + /** + * The decompressed filesize in bytes module 2^32, if available. + * Or -1 if the filesize is not available. + */ + int32_t dlen = -1; + + /** + * A single decompressed byte, decompressed in advance to work around the incorrect file end detection of uzlib. + */ + int16_t dcbuf = -1; + +public: + /** + * Creates a new gzip wrapper to decompress the gzip file in the given memory block. + * + * For this the entire compressed file has to be in a single continuous memory block. + * + * With this constructor the uncompressed size is available immediately. + * + * @param cmp_start A pointer to the first byte of the gzip file. + * @param cmp_end A pointer to the first byte after the gzip file. + * @param wsize The window size used for decompression. + * Has to be at least as much as the window size used for compression. + * A pow(2, -wsize) byte buffer is allocated for decompression. + * The range of valid values is from -8 to -15. + * Values outside of this range will be clamped to this range. + */ + uzlib_ungzip_wrapper(const uint8_t *cmp_start, const uint8_t *cmp_end, + int8_t wsize); + + /** + * Creates a new gzip wrapper to decompress a gzip file from a callback. + * + * This callback will be called each time `uzlib_uncomp->source` is empty. + * This means by default it will be called for every byte to read. + * + * It is however allowed to modify `uzlib_uncomp->source` and `uzlib_uncomp->source_limit` + * in this callback to allow reading a chunk of data at once. + * In this case the callback still has to return the **FIRST** read byte. + * If `uzlib_uncomp->source` is modified, it must point to the next byte after the returned byte. + * + * Once all bytes are read the callback must return -1. + * + * **WARNING:** This callback **MUST NOT** return the last four bytes of a gzip file, since those can't be decompressed. + * + * With this constructor the uncompressed size is only available once the file is read in its entirety. + * + * @param callback The callback to get the compressed data from. + * @param wsize The window size used for decompression. + * Has to be at least as much as the window size used for compression. + * A pow(2, -wsize) byte buffer is allocated for decompression. + * The range of valid values is from -8 to -15. + * Values outside of this range will be clamped to this range. + */ + uzlib_ungzip_wrapper(int (*callback)(uzlib_uncomp*), int8_t wsize); + + /** + * Destroys this gzip wrapper, and removes its internal memory buffer. + */ + ~uzlib_ungzip_wrapper(); + + /** + * Decompresses the next segment of the gzip file to the given memory buffer. + * + * Attempts to decompress a single additional byte in advance, + * to work around uzlib not detecting the file end when the buffer size exactly matches the uncompressed size. + * + * @param buf The memory buffer to write to. + * @param buf_size The max number of bytes to write to the buffer. + * @return The number of bytes written to the buffer. + */ + size_t decompress(uint8_t *buf, const size_t buf_size); + + /** + * Gets the uncompressed size of the file to be decompressed, if available. + * + * The uncompressed size is always available once decompression is finished. + * If this wrapper was initialized with a memory block, rather than a callback, + * the uncompressed size is immediately available. + * + * @return The uncompressed file size. + */ + int32_t getDecompressedSize() const; + + /** + * Gets the number of bytes that were already decompressed. + * + * @return The number of already decompressed bytes. + */ + uint32_t getDecompressed() const; + + /** + * Checks whether the file was decompressed in its entirety. + * + * @return Whether the decompression is done. + */ + bool done() const; +}; + +} /* namespace gzip */ + +#endif /* SRC_HTML_UZLIB_GZIP_WRAPPER_H_ */ diff --git a/lib/uzlib_gzip_wrapper/library.json b/lib/uzlib_gzip_wrapper/library.json new file mode 100644 index 0000000..6b06fc9 --- /dev/null +++ b/lib/uzlib_gzip_wrapper/library.json @@ -0,0 +1,16 @@ +{ + "name": "UZLibGzipWrapper", + "description": "A wrapper to help with storing the data associated with (de)compressing a GZIP file using UZLib. Can't handle compressing yet.", + "version": "1.0.0", + "license": "MIT", + "dependencies": [ + { + "name": "uzlib", + "version": "https://github.com/pfalcon/uzlib.git" + }, + { + "name": "FallbackLog", + "version": "FallbackLog" + } + ] +} diff --git a/lib/uzlib_gzip_wrapper/src/uzlib_gzip_wrapper.cpp b/lib/uzlib_gzip_wrapper/src/uzlib_gzip_wrapper.cpp new file mode 100644 index 0000000..32b234d --- /dev/null +++ b/lib/uzlib_gzip_wrapper/src/uzlib_gzip_wrapper.cpp @@ -0,0 +1,133 @@ +/* + * uzlib_gzip_wrapper.cpp + * + * Created on: May 26, 2023 + * + * Copyright (C) 2023 ToMe25. + * This project is licensed under the MIT License. + * The MIT license can be found in the project root and at https://opensource.org/licenses/MIT. + */ + +#include "uzlib_gzip_wrapper.h" +#ifdef ARDUINO +#include +#endif +#include +#include + +namespace gzip { + +void init() { + uzlib_init(); +} + +uzlib_ungzip_wrapper::uzlib_ungzip_wrapper(const uint8_t *cmp_start, + const uint8_t *cmp_end, int8_t wsize) { + if (wsize > -8) { + log_e("Window size out of range."); + wsize = -8; + } else if (wsize < -15) { + log_e("Window size out of range."); + wsize = -15; + } + + decomp = new uzlib_uncomp; + void *dict = malloc(pow(2, -wsize)); + // Try anyways, since small files can be decompressed without one. + if (!dict) { + log_e("Failed to allocate decompression dict."); + } + + // Read uncompressed size from compressed file. + dlen = cmp_end[-1]; + dlen = 256 * dlen + cmp_end[-2]; + dlen = 256 * dlen + cmp_end[-3]; + dlen = 256 * dlen + cmp_end[-4]; + + uzlib_uncompress_init(decomp, dict, pow(2, -wsize)); + decomp->source = cmp_start; + decomp->source_limit = cmp_end - 4; + decomp->source_read_cb = NULL; + uzlib_gzip_parse_header(decomp); +} + +uzlib_ungzip_wrapper::uzlib_ungzip_wrapper(int (*callback)(uzlib_uncomp*), + int8_t wsize) { + if (wsize > -8) { + log_e("Window size out of range."); + wsize = -8; + } else if (wsize < -15) { + log_e("Window size out of range."); + wsize = -15; + } + + decomp = new uzlib_uncomp; + void *dict = malloc(pow(2, -wsize)); + // Try anyways, since small files can be decompressed without one. + if (!dict) { + log_e("Failed to allocate decompression dict."); + } + + uzlib_uncompress_init(decomp, dict, pow(2, -wsize)); + decomp->source = NULL; + decomp->source_limit = NULL; + decomp->source_read_cb = callback; + uzlib_gzip_parse_header(decomp); +} + +uzlib_ungzip_wrapper::~uzlib_ungzip_wrapper() { + free(decomp->dict_ring); + delete decomp; +} + +size_t uzlib_ungzip_wrapper::decompress(uint8_t *buf, const size_t buf_size) { + if (decomp->eof) { + return 0; + } + + decomp->dest = buf; + decomp->dest_limit = buf + buf_size; + if (dcbuf >= 0) { + *buf = (uint8_t) dcbuf; + decomp->dest = buf + 1; + } + + int res = uzlib_uncompress_chksum(decomp); + if (res != TINF_OK && res != TINF_DONE) { + log_e("Decompress failed with error %d.", res); + } + + const size_t read = decomp->dest - buf; + index += read; + if (!decomp->eof) { + unsigned char dbuf = 0; + decomp->dest = &dbuf; + decomp->dest_limit = decomp->dest + 1; + res = uzlib_uncompress_chksum(decomp); + if (res == TINF_OK) { + dcbuf = dbuf; + } else if (res != TINF_DONE) { + log_e("Decompress failed with error %d.", res); + } + } + + if (decomp->eof) { + dlen = index; + } + + return read; +} + +int32_t uzlib_ungzip_wrapper::getDecompressedSize() const { + return dlen; +} + +uint32_t uzlib_ungzip_wrapper::getDecompressed() const { + return index; +} + +bool uzlib_ungzip_wrapper::done() const { + return decomp->eof; +} + +} /* namespace gzip */ diff --git a/platformio.ini b/platformio.ini index 1af3bbb..09ad382 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,6 +8,22 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html +[platformio] +name = "ESP WiFi Thermometer" +default_envs = + esp32dev + esp32dev_debug + esp32dev_ota + esp32dev_ota_debug + esp32dev_dsm + esp32dev_dsm_debug + esp_wroom_02 + esp_wroom_02_debug + esp_wroom_02_ota + esp_wroom_02_ota_debug + esp_wroom_02_dsm + esp_wroom_02_dsm_debug + [env] framework = arduino monitor_speed = 115200 @@ -15,11 +31,10 @@ upload_speed = 921600 extra_scripts = post:shared/compress_web.py lib_deps = ArduinoOTA - ESP Async WebServer = https://github.com/me-no-dev/ESPAsyncWebServer.git + ESPAsyncWebServer = https://github.com/me-no-dev/ESPAsyncWebServer.git marvinroger/AsyncMqttClient@^0.9.0 adafruit/DHT sensor library@^1.4.4 milesburton/DallasTemperature@^3.11.0 - uzlib = https://github.com/pfalcon/uzlib.git board_build.embed_txtfiles = wifissid.txt wifipass.txt @@ -35,11 +50,23 @@ board_build.embed_files = data/gzip/favicon.ico.gz data/gzip/favicon.png.gz data/gzip/favicon.svg.gz +test_framework = unity [debug] build_type = debug build_flags = -D CORE_DEBUG_LEVEL=5 +[env:native] +platform = native +framework = +lib_deps = UZLibGzipWrapper + +[env:native_debug] +extends = env:native, debug +build_flags = + ${env:native.build_flags} + ${debug.build_flags} + [env:esp32dev] platform = espressif32@^6.4.0 board = esp32dev diff --git a/src/AsyncTrackingFallbackWebHandler.cpp b/src/AsyncTrackingFallbackWebHandler.cpp index 73934e2..b8b191c 100644 --- a/src/AsyncTrackingFallbackWebHandler.cpp +++ b/src/AsyncTrackingFallbackWebHandler.cpp @@ -13,13 +13,13 @@ #if ENABLE_PROMETHEUS_SCRAPE_SUPPORT == 1 || ENABLE_PROMETHEUS_PUSH == 1 #include "prometheus.h" #endif -#include "utils.h" -#include "fallback_log.h" +#include +#include web::AsyncTrackingFallbackWebHandler::AsyncTrackingFallbackWebHandler( const String &uri, HTTPFallbackRequestHandler fallback) : _uri(uri), _fallbackHandler(fallback), _handlers( - utility::get_msb(HTTP_ANY) + 1) { + utils::get_msb(HTTP_ANY) + 1) { } @@ -28,7 +28,7 @@ web::AsyncTrackingFallbackWebHandler::~AsyncTrackingFallbackWebHandler() { } web::HTTPRequestHandler web::AsyncTrackingFallbackWebHandler::_getHandler(const WebRequestMethod method) const { - return _handlers[utility::get_msb((WebRequestMethodComposite) method)]; + return _handlers[utils::get_msb((WebRequestMethodComposite) method)]; } void web::AsyncTrackingFallbackWebHandler::setHandler(const WebRequestMethodComposite methods, HTTPRequestHandler handler) { diff --git a/src/config.h b/src/config.h index 1721756..04cca49 100644 --- a/src/config.h +++ b/src/config.h @@ -11,13 +11,12 @@ #ifndef SRC_CONFIG_H_ #define SRC_CONFIG_H_ -#include #ifdef ESP32 #include #elif defined(ESP8266) #include #endif -#include "utils.h" +#include /** * This file contains a few variables and defines to be used as config values. @@ -176,7 +175,7 @@ static constexpr uint8_t SENSOR_PIN = 5; // The default namespace is "esptherm". static constexpr const char PROMETHEUS_NAMESPACE[] = "esptherm"; // The length of the prometheus namespace string. -static constexpr size_t PROMETHEUS_NAMESPACE_LEN = utility::strlen(PROMETHEUS_NAMESPACE); +static constexpr size_t PROMETHEUS_NAMESPACE_LEN = utils::strlen(PROMETHEUS_NAMESPACE); // Whether the esp should automatically push measurements to a prometheus-pushgateway. // This is done through HTTP post requests to a given address at fixed intervals. // Set to 1 to enable and to 0 to disable. @@ -200,18 +199,18 @@ static constexpr uint16_t PROMETHEUS_PUSH_INTERVAL = 30; // The default is to use the device hostname. static constexpr const char PROMETHEUS_PUSH_JOB[] = ""; // The length of the prometheus pushgateway job. -static constexpr size_t PROMETHEUS_PUSH_JOB_LEN = utility::strlen(PROMETHEUS_PUSH_JOB); +static constexpr size_t PROMETHEUS_PUSH_JOB_LEN = utils::strlen(PROMETHEUS_PUSH_JOB); // The name of the instance to use for the prometheus metrics when pushing. // Leave empty to use the device IP. static constexpr const char PROMETHEUS_PUSH_INSTANCE[] = ""; // The length of the prometheus pushgateway instance string. -static constexpr size_t PROMETHEUS_PUSH_INSTANCE_LEN = utility::strlen(PROMETHEUS_PUSH_INSTANCE); +static constexpr size_t PROMETHEUS_PUSH_INSTANCE_LEN = utils::strlen(PROMETHEUS_PUSH_INSTANCE); // The name of the namespace to use for the prometheus metrics when pushing. // Leave empty to use the prometheus namespace. // The default is to use the prometheus namespace. static constexpr const char PROMETHEUS_PUSH_NAMESPACE[] = ""; // The length of the prometheus pushgateway namespace string. -static constexpr size_t PROMETHEUS_PUSH_NAMESPACE_LEN = utility::strlen(PROMETHEUS_PUSH_NAMESPACE); +static constexpr size_t PROMETHEUS_PUSH_NAMESPACE_LEN = utils::strlen(PROMETHEUS_PUSH_NAMESPACE); #endif // MQTT options diff --git a/src/main.cpp b/src/main.cpp index fc3637c..b27c8ed 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,7 +19,7 @@ #if defined(ESP8266) #include #endif -#include "fallback_log.h" +#include IPAddress localhost; #ifdef ESP32 @@ -246,7 +246,7 @@ void loop() { if (sensors::SENSOR_HANDLER.supportsHumidity()) { Serial.print("Relative Humidity: "); Serial.print( - utility::float_to_string( + utils::float_to_string( sensors::SENSOR_HANDLER.getHumidity(), 2).c_str()); if (!std::isnan(sensors::SENSOR_HANDLER.getHumidity())) { Serial.println('%'); @@ -314,8 +314,7 @@ bool handle_serial_input(const std::string &input) { Serial.println(); Serial.print("Relative humidity: "); Serial.print( - utility::float_to_string(sensors::SENSOR_HANDLER.getHumidity(), - 2).c_str()); + utils::float_to_string(sensors::SENSOR_HANDLER.getHumidity(), 2).c_str()); if (!std::isnan(sensors::SENSOR_HANDLER.getHumidity())) { Serial.println('%'); } else { @@ -364,10 +363,10 @@ bool handle_serial_input(const std::string &input) { void printTemperature(Print &out, const float temp) { out.print("Temperature: "); if (!std::isnan(temp)) { - out.print(utility::float_to_string(temp, 2).c_str()); + out.print(utils::float_to_string(temp, 2).c_str()); out.print("°C, "); out.print( - utility::float_to_string(utility::celsiusToFahrenheit(temp), 2).c_str()); + utils::float_to_string(utils::celsiusToFahrenheit(temp), 2).c_str()); out.println("°F"); } else { out.println("Unknown"); diff --git a/src/prometheus.cpp b/src/prometheus.cpp index 8677ae9..f759f22 100644 --- a/src/prometheus.cpp +++ b/src/prometheus.cpp @@ -13,7 +13,7 @@ #include "sensor_handler.h" #include #include -#include "fallback_log.h" +#include #if ENABLE_WEB_SERVER == 1 && (ENABLE_PROMETHEUS_PUSH == 1 || ENABLE_PROMETHEUS_SCRAPE_SUPPORT == 1) std::map, uint64_t>> prom::http_requests_total; diff --git a/src/sensor_handler.cpp b/src/sensor_handler.cpp index 1074009..9fbc03d 100644 --- a/src/sensor_handler.cpp +++ b/src/sensor_handler.cpp @@ -10,7 +10,7 @@ #include "config.h" #include "sensor_handler.h" -#include "utils.h" +#include #if SENSOR_TYPE == SENSOR_TYPE_DHT #include "sensors/DHTHandler.h" #elif SENSOR_TYPE == SENSOR_TYPE_DALLAS @@ -27,19 +27,19 @@ SensorHandler::~SensorHandler() { } const std::string SensorHandler::getTemperatureString() { - return utility::float_to_string(getTemperature(), 2); + return utils::float_to_string(getTemperature(), 2); } const std::string SensorHandler::getLastTemperatureString() { - return utility::float_to_string(getLastTemperature(), 2); + return utils::float_to_string(getLastTemperature(), 2); } const std::string SensorHandler::getHumidityString() { - return utility::float_to_string(getHumidity(), 2); + return utils::float_to_string(getHumidity(), 2); } const std::string SensorHandler::getLastHumidityString() { - return utility::float_to_string(getLastHumidity(), 2); + return utils::float_to_string(getLastHumidity(), 2); } int64_t SensorHandler::getTimeSinceMeasurement() { @@ -59,11 +59,11 @@ int64_t SensorHandler::getTimeSinceValidMeasurement() { } const std::string SensorHandler::getTimeSinceMeasurementString() { - return utility::timespan_to_string(getTimeSinceMeasurement()); + return utils::timespan_to_string(getTimeSinceMeasurement()); } const std::string SensorHandler::getTimeSinceValidMeasurementString() { - return utility::timespan_to_string(getTimeSinceValidMeasurement()); + return utils::timespan_to_string(getTimeSinceValidMeasurement()); } uint16_t SensorHandler::getMinInterval() const { diff --git a/src/sensors/DHTHandler.cpp b/src/sensors/DHTHandler.cpp index eac8452..bcc8360 100644 --- a/src/sensors/DHTHandler.cpp +++ b/src/sensors/DHTHandler.cpp @@ -9,7 +9,7 @@ */ #include "sensors/DHTHandler.h" -#include "fallback_log.h" +#include namespace sensors { diff --git a/src/sensors/DallasHandler.cpp b/src/sensors/DallasHandler.cpp index 192533f..ec35316 100644 --- a/src/sensors/DallasHandler.cpp +++ b/src/sensors/DallasHandler.cpp @@ -9,7 +9,7 @@ */ #include "sensors/DallasHandler.h" -#include "fallback_log.h" +#include namespace sensors { diff --git a/src/uzlib_gzip_wrapper.cpp b/src/uzlib_gzip_wrapper.cpp deleted file mode 100644 index 5979a4b..0000000 --- a/src/uzlib_gzip_wrapper.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* - * uzlib_gzip_wrapper.cpp - * - * Created on: May 26, 2023 - * - * Copyright (C) 2023 ToMe25. - * This project is licensed under the MIT License. - * The MIT license can be found in the project root and at https://opensource.org/licenses/MIT. - */ - -#include "uzlib_gzip_wrapper.h" -#include -#include "fallback_log.h" - -uzlib_gzip_wrapper::uzlib_gzip_wrapper(const uint8_t *cmp_start, - const uint8_t *cmp_end, int8_t wsize) : - cmp_start(cmp_start) { - if (wsize > -8) { - log_e("Window size out of range."); - wsize = -8; - } else if (wsize < -15) { - log_e("Window size out of range."); - wsize = -15; - } - - decomp = new uzlib_uncomp; - void *dict = malloc(pow(2, -wsize)); - // Try anyways, since small files can be decompressed without one. - if (!dict) { - log_e("Failed to allocate decompression dict."); - } - uzlib_uncompress_init(decomp, dict, pow(2, -wsize)); - decomp->source = cmp_start; - decomp->source_limit = cmp_end - 4; - decomp->source_read_cb = NULL; - uzlib_gzip_parse_header(decomp); -} - -uzlib_gzip_wrapper::~uzlib_gzip_wrapper() { - free(decomp->dict_ring); - delete decomp; -} - -uint32_t uzlib_gzip_wrapper::getDecompressedSize() const { - size_t dlen = decomp->source_limit[3]; - dlen = 256 * dlen + decomp->source_limit[2]; - dlen = 256 * dlen + decomp->source_limit[1]; - dlen = 256 * dlen + decomp->source_limit[0]; - return dlen; -} - -size_t uzlib_gzip_wrapper::decompress(uint8_t *buf, const size_t buf_size) { - decomp->dest = buf; - decomp->dest_limit = buf + buf_size; - int res = uzlib_uncompress(decomp); - if (res != TINF_OK && res != TINF_DONE) { - log_e("Decompress failed with error %d.", res); - } - return decomp->dest - buf; -} diff --git a/src/uzlib_gzip_wrapper.h b/src/uzlib_gzip_wrapper.h deleted file mode 100644 index 15d7f5d..0000000 --- a/src/uzlib_gzip_wrapper.h +++ /dev/null @@ -1,74 +0,0 @@ -/* - * uzlib_gzip_wrapper.h - * - * Created on: May 26, 2023 - * - * Copyright (C) 2023 ToMe25. - * This project is licensed under the MIT License. - * The MIT license can be found in the project root and at https://opensource.org/licenses/MIT. - */ - -#ifndef SRC_HTML_UZLIB_GZIP_WRAPPER_H_ -#define SRC_HTML_UZLIB_GZIP_WRAPPER_H_ - -#include - -/** - * A wrapper to help with storing the data associated with decompressing a gzip file using uzlib. - * Can currently only handle the entire compressed file being accessible as a single pointer block. - */ -class uzlib_gzip_wrapper { -private: - /** - * The internal data store used by uzlib. - */ - uzlib_uncomp *decomp; - - /** - * A pointer to the first byte of compressed data. - */ - const uint8_t *const cmp_start; - - /** - * The number of already decompressed bytes. - */ - size_t index = 0; - -public: - /** - * Creates a new gzip wrapper to compress the gzip file in the given memory block. - * - * @param cmp_start A pointer to the first byte of the gzip file. - * @param cmp_end A pointer to the first byte after the gzip file. - * @param wsize The window size used for decompression. - * Has to be at least as much as the window size used for compression. - * A pow(2, -wsize) byte buffer is allocated for decompression. - * The range of valid values is from -8 to -15. - * Values outside of this range will be clamped to this range. - */ - uzlib_gzip_wrapper(const uint8_t *cmp_start, const uint8_t *cmp_end, - int8_t wsize); - - /** - * Destroys this gzip wrapper, and removes its internal memory buffer. - */ - ~uzlib_gzip_wrapper(); - - /** - * Decompresses the next segment of the gzip file to the given memory buffer. - * - * @param buf The memory buffer to write to. - * @param buf_size The max number of bytes to write to the buffer. - * @return The number of bytes written to the buffer. - */ - size_t decompress(uint8_t *buf, const size_t buf_size); - - /** - * Gets the uncompressed size of the file to be decompressed. - * - * @return The uncompressed file size. - */ - uint32_t getDecompressedSize() const; -}; - -#endif /* SRC_HTML_UZLIB_GZIP_WRAPPER_H_ */ diff --git a/src/webhandler.cpp b/src/webhandler.cpp index 41cf145..d205458 100644 --- a/src/webhandler.cpp +++ b/src/webhandler.cpp @@ -20,7 +20,7 @@ #elif defined(ESP8266) #include #endif -#include "fallback_log.h" +#include #endif /* ENABLE_WEB_SERVER == 1 */ #if ENABLE_WEB_SERVER == 1 @@ -87,7 +87,7 @@ void web::setup() { DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); server.begin(); - uzlib_init(); + gzip::init(); #if ENABLE_ARDUINO_OTA != 1 MDNS.begin(HOSTNAME); @@ -152,8 +152,8 @@ web::ResponseData web::getJson(AsyncWebServerRequest *request) { } size_t web::decompressingResponseFiller( - const std::shared_ptr decomp, uint8_t *buffer, - const size_t max_len, const size_t index) { + const std::shared_ptr decomp, + uint8_t *buffer, const size_t max_len, const size_t index) { return decomp->decompress(buffer, max_len); } @@ -375,11 +375,11 @@ web::ResponseData web::compressedStaticHandler(const uint16_t status_code, response->addHeader("Content-Encoding", "gzip"); } else { using namespace std::placeholders; - std::shared_ptr decomp = std::make_shared< - uzlib_gzip_wrapper>(start, end, GZIP_DECOMP_WINDOW_SIZE); + std::shared_ptr decomp = std::make_shared< + gzip::uzlib_ungzip_wrapper>(start, end, + GZIP_DECOMP_WINDOW_SIZE); content_length = decomp->getDecompressedSize(); - response = request->beginResponse(content_type, - decomp->getDecompressedSize(), + response = request->beginResponse(content_type, content_length, std::bind(decompressingResponseFiller, decomp, _1, _2, _3)); } response->setCode(status_code); diff --git a/src/webhandler.h b/src/webhandler.h index bc54a63..ee5d3bb 100644 --- a/src/webhandler.h +++ b/src/webhandler.h @@ -37,8 +37,8 @@ typedef std::function< AsyncWebServerRequest *request)> HTTPFallbackRequestHandler; } -#include -#include "uzlib_gzip_wrapper.h" +#include "AsyncTrackingFallbackWebHandler.h" +#include #include extern const char INDEX_HTML_START[] asm("_binary_data_index_html_start"); @@ -147,8 +147,8 @@ ResponseData getJson(AsyncWebServerRequest *request); * @return The number of bytes written to the output buffer. */ size_t decompressingResponseFiller( - const std::shared_ptr decomp, uint8_t *buffer, - const size_t max_len, const size_t index); + const std::shared_ptr decomp, + uint8_t *buffer, const size_t max_len, const size_t index); /** * A AwsResponseFiller copying the given static file, and replacing the given template strings. diff --git a/test/README b/test/README deleted file mode 100644 index b94d089..0000000 --- a/test/README +++ /dev/null @@ -1,11 +0,0 @@ - -This directory is intended for PlatformIO Unit Testing and project tests. - -Unit Testing is a software testing method by which individual units of -source code, sets of one or more MCU program modules together with associated -control data, usage procedures, and operating procedures, are tested to -determine whether they are fit for use. Unit testing finds problems early -in the development cycle. - -More information about PlatformIO Unit Testing: -- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/test/test_uzlib_gzip_wrapper/decompress.cpp b/test/test_uzlib_gzip_wrapper/decompress.cpp new file mode 100644 index 0000000..6b58dde --- /dev/null +++ b/test/test_uzlib_gzip_wrapper/decompress.cpp @@ -0,0 +1,479 @@ +/* + * decompress.cpp + * + * Created on: Jan 5, 2024 + * + * Copyright (C) 2023 ToMe25. + * This project is licensed under the MIT License. + * The MIT license can be found in the project root and at https://opensource.org/licenses/MIT. + */ + +#include +#include +#include +#include +#include +#include +#include + +/** + * Use a constant seed, to get reproducible results. + * Used to generate the random data for the files for compression tests. + */ +const std::mt19937::result_type RANDOM_SEED = 1685018244; + +/** + * The characters to be used as part of the generated random data. + */ +const char RANDOM_CHARS[] = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +/** + * The number of random characters to use. + */ +constexpr size_t RANDOM_CHARS_LEN = utils::strlen(RANDOM_CHARS); + +/** + * The command to be used to compress a file, without the filename component. + * A window size of -10 means a 1024 byte buffer, while -15(the max) means a 32768 byte buffer. + */ +const char BASE_COMMAND[] = + "python3 -m shared.gzip_compressing_stream --window-size -10 "; + +/** + * The path to the uncompressed random data file. + * Initialized in setUp and deleted in tearDown. + */ +char *uncompressed_path; + +/** + * The path for the compressed gzip file. + * Initialized in setUp and deleted in tearDown. + */ +char *compressed_path; + +/** + * The command to execute to compress the uncompressed file. + * Initialized in setUp and deleted in tearDown. + */ +char *compress_command; + +/** + * The integer distribution for the random data. + * Initialized in setUp and destroyed in tearDown. + */ +std::uniform_int_distribution *distribution; + +/** + * A pointer to the random number generator to be used. + * Initialized in setUp and destroyed in tearDown. + */ +std::mt19937 *rng; + +/** + * A pointer to a file input stream for the compressed file. + * To be populated by the test. Will be deleted in tearDown. + */ +std::ifstream *compressed_in = NULL; + +/** + * The length of the compressed file. + * Has to be determined and set by the test. + */ +size_t compressed_length = 0; + +/** + * Generates a new path for the uncompressed file, and initializes the compressed path to match it. + * Also initializes the random number generator. + */ +void setUp() { + uncompressed_path = new char[L_tmpnam]; + uncompressed_path = tmpnam(uncompressed_path); + if (uncompressed_path) { + const size_t u_len = std::strlen(uncompressed_path); + compressed_path = new char[u_len + 4]; // 3 for .gz plus NUL byte. + strncpy(compressed_path, uncompressed_path, u_len); + strncpy(compressed_path + u_len, ".gz", 4); + + const size_t bc_len = std::strlen(BASE_COMMAND); + compress_command = new char[bc_len + u_len + 1]; + strncpy(compress_command, BASE_COMMAND, bc_len); + strncpy(compress_command + bc_len, uncompressed_path, u_len + 1); + } + + rng = new std::mt19937(RANDOM_SEED); + distribution = new std::uniform_int_distribution(0, + RANDOM_CHARS_LEN - 1); +} + +/** + * Deletes the compressed and uncompressed file, and destroys the random number generator. + */ +void tearDown() { + remove(uncompressed_path); + uncompressed_path = NULL; + remove(compressed_path); + compressed_path = NULL; + delete rng; + rng = NULL; + delete distribution; + distribution = NULL; + delete compressed_in; + compressed_in = NULL; +} + +/** + * Checks that the temporary path was successfully generated, and a python interpreter is available. + */ +void check_fixtures() { + TEST_ASSERT_NOT_NULL_MESSAGE(uncompressed_path, + "Failed to generate temporary file path."); + TEST_ASSERT_NOT_NULL_MESSAGE(compressed_path, + "Failed to generate temporary file path."); + TEST_ASSERT_NOT_NULL_MESSAGE(compress_command, + "Failed to assemble command string."); + + // Assume that a successful command returns 0. + TEST_ASSERT_EQUAL_INT_MESSAGE(0, std::system("python3 -V"), + "Failed to find python 3 interpreter."); +} + +/** + * Writes the given number of random bytes to the given file output stream. + * + * This function generates only bytes matching this regex: `[0-9a-zA-Z]`. + * + * @param output The file output stream to write to. + * @param bytes The number of bytes to write. + */ +void write_random_data(std::ofstream &output, const uint32_t bytes) { + for (size_t i = 0; i < bytes; i++) { + output << RANDOM_CHARS[(*distribution)(*rng)]; + } +} + +/** + * Creates an uncompressed file with random data of the given size, and compresses it. + * + * @param size The size of the uncompressed file, in bytes. + */ +void prepare_compressed_file(const size_t size) { + std::ofstream uncompressed_out; + uncompressed_out.open(uncompressed_path); + TEST_ASSERT_TRUE_MESSAGE(uncompressed_out.is_open(), + "Failed to open uncompressed file."); + write_random_data(uncompressed_out, size); + uncompressed_out.close(); + TEST_ASSERT_FALSE_MESSAGE(uncompressed_out.fail(), + "Writing uncompressed file failed."); + + // Assume that a successful command returns 0. + TEST_ASSERT_EQUAL_INT_MESSAGE(0, std::system(compress_command), + "GZIP compression command failed."); +} + +/** + * Test decompressing a file that is small enough to fit into the decompression buffer. + */ +void test_decompress_small() { + // The size of the uncompressed file used for testing. + const size_t FILE_SIZE = 512; + // The size of the block of data to be compared when checking the file. + const size_t TEST_COMP_SIZE = 25; + + check_fixtures(); + prepare_compressed_file(FILE_SIZE); + + compressed_in = new std::ifstream(); + compressed_in->open(compressed_path, std::ios::in | std::ios::binary); + TEST_ASSERT_TRUE_MESSAGE(compressed_in->is_open(), + "Failed to open compressed file."); + char *compressed = new char[FILE_SIZE]; + compressed_length = compressed_in->read(compressed, FILE_SIZE).gcount(); + compressed_in->close(); + + gzip::uzlib_ungzip_wrapper unzip((uint8_t*) compressed, + (uint8_t*) compressed + compressed_length, -10); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(FILE_SIZE, unzip.getDecompressedSize(), + "The initial decompressed size didn't match expectations."); + std::ifstream uncompressed_in; + uncompressed_in.open(uncompressed_path); + TEST_ASSERT_TRUE_MESSAGE(uncompressed_in.is_open(), + "Failed to open uncompressed file."); + char *uncompressed = new char[FILE_SIZE]; + uncompressed_in.read(uncompressed, FILE_SIZE); + uncompressed_in.close(); + + char *decompressed = new char[FILE_SIZE]; + TEST_ASSERT_EQUAL_UINT_MESSAGE(FILE_SIZE, + unzip.decompress((uint8_t* ) decompressed, FILE_SIZE), + "The number of decompressed bytes didn't match the decompressed size."); + TEST_ASSERT_TRUE_MESSAGE(unzip.done(), + "The decompression wasn't considered done after decompressing everything."); + TEST_ASSERT_EQUAL_INT32_MESSAGE(FILE_SIZE, unzip.getDecompressedSize(), + "The final decompressed size didn't match expectations."); + + // Compare content in blocks of TEST_COMP_SIZE characters, for better error readability. + const size_t FILE_SIZE_LEN = log10(FILE_SIZE) + 1; + for (size_t start = 0; start + TEST_COMP_SIZE < FILE_SIZE; start += + TEST_COMP_SIZE) { + const size_t comp_len = std::min(TEST_COMP_SIZE, FILE_SIZE - start); + char message[58 + FILE_SIZE_LEN * 2]; + snprintf(message, 58 + FILE_SIZE_LEN * 2, + "Decompressed file bytes %lu-%lu don't match uncompressed file.", + start, start + comp_len); + TEST_ASSERT_EQUAL_STRING_LEN_MESSAGE(uncompressed + start, + decompressed + start, comp_len, message); + } +} + +/** + * Test decompressing a file that too large to fit into the decompressing buffer, from memory to memory. + */ +void test_decompress_large() { + // The size of the uncompressed file used for testing. + const size_t FILE_SIZE = 131072; + // The size of the block of data to be compared when checking the file. + const size_t TEST_COMP_SIZE = 25; + + check_fixtures(); + prepare_compressed_file(FILE_SIZE); + + compressed_in = new std::ifstream(); + compressed_in->open(compressed_path, std::ios::in | std::ios::binary); + TEST_ASSERT_TRUE_MESSAGE(compressed_in->is_open(), + "Failed to open compressed file."); + char *compressed = new char[FILE_SIZE]; + compressed_length = compressed_in->read(compressed, FILE_SIZE).gcount(); + compressed_in->close(); + + gzip::uzlib_ungzip_wrapper unzip((uint8_t*) compressed, + (uint8_t*) compressed + compressed_length, -10); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(FILE_SIZE, unzip.getDecompressedSize(), + "The initial decompressed size didn't match expectations."); + std::ifstream uncompressed_in; + uncompressed_in.open(uncompressed_path); + TEST_ASSERT_TRUE_MESSAGE(uncompressed_in.is_open(), + "Failed to open uncompressed file."); + char *uncompressed = new char[FILE_SIZE]; + uncompressed_in.read(uncompressed, FILE_SIZE); + uncompressed_in.close(); + + char *decompressed = new char[FILE_SIZE]; + TEST_ASSERT_EQUAL_UINT_MESSAGE(FILE_SIZE, + unzip.decompress((uint8_t* ) decompressed, FILE_SIZE), + "The number of decompressed bytes didn't match the decompressed size."); + TEST_ASSERT_TRUE_MESSAGE(unzip.done(), + "The decompression wasn't considered done after decompressing everything."); + TEST_ASSERT_EQUAL_INT32_MESSAGE(FILE_SIZE, unzip.getDecompressedSize(), + "The final decompressed size didn't match expectations."); + + // Compare content in blocks of TEST_COMP_SIZE characters, for better error readability. + const size_t FILE_SIZE_LEN = log10(FILE_SIZE) + 1; + for (size_t start = 0; start + TEST_COMP_SIZE < FILE_SIZE; start += + TEST_COMP_SIZE) { + const size_t comp_len = std::min(TEST_COMP_SIZE, FILE_SIZE - start); + char message[58 + FILE_SIZE_LEN * 2]; + snprintf(message, 58 + FILE_SIZE_LEN * 2, + "Decompressed file bytes %lu-%lu don't match uncompressed file.", + start, start + comp_len); + TEST_ASSERT_EQUAL_STRING_LEN_MESSAGE(uncompressed + start, + decompressed + start, comp_len, message); + } +} + +/** + * The uzlib read callback for test_decompress_streaming. + * + * @param uncomp The uzlib_uncomp instance to read data for. + * @return The first read byte, or -1 if the file was read in its entirety already. + */ +int read_compressed_callback(uzlib_uncomp *uncomp) { + // The size of the internal read buffer. + const size_t RDBUFSIZ = 1024; + + if (compressed_in->eof() + || compressed_in->tellg() >= compressed_length - 4) { + if (uncomp->source != NULL) { + uncomp->source -= (compressed_length - 4) % RDBUFSIZ - 1; + delete[] (uncomp->source - 1); + uncomp->source = NULL; + uncomp->source_limit = NULL; + } + return -1; + } + + if (uncomp->source == NULL) { + uncomp->source = new unsigned char[RDBUFSIZ]; + uncomp->source += 1; + uncomp->source_limit = uncomp->source + RDBUFSIZ - 1; + } else { + uncomp->source -= RDBUFSIZ - 1; + } + + int read = + compressed_in->read((char*) uncomp->source - 1, + std::min((int64_t) RDBUFSIZ, + (int64_t) (compressed_length + - compressed_in->tellg() - 4))).gcount(); + + if (read == 0) { + if (uncomp->source != NULL) { + uncomp->source += RDBUFSIZ - 1; + uncomp->source -= (compressed_length - 4) % RDBUFSIZ - 1; + delete[] (uncomp->source - 1); + uncomp->source = NULL; + uncomp->source_limit = NULL; + } + return -1; + } + + uncomp->source_limit = uncomp->source + read - 1; + return uncomp->source[-1]; +} + +/** + * Test decompressing a file that is too big to be stored completely in memory. + */ +void test_decompress_streaming() { + // The size of the uncompressed file used for testing. + const size_t FILE_SIZE = 536870912; + // The size of the block of data to be compared when checking the file. + const size_t TEST_COMP_SIZE = 25; + // The size of the read and decompression buffers. + const size_t BUFFER_SIZE = 4096; + + check_fixtures(); + prepare_compressed_file(FILE_SIZE); + + compressed_in = new std::ifstream(); + compressed_in->open(compressed_path, std::ios::in | std::ios::binary); + TEST_ASSERT_TRUE_MESSAGE(compressed_in->is_open(), + "Failed to open compressed file."); + compressed_in->seekg(0, std::ios::end); + compressed_length = compressed_in->tellg(); + compressed_in->seekg(0, std::ios::beg); + + gzip::uzlib_ungzip_wrapper unzip(&read_compressed_callback, -10); + TEST_ASSERT_EQUAL_INT32_MESSAGE(-1, unzip.getDecompressedSize(), + "The initial decompressed size didn't match expectations."); + std::ifstream uncompressed_in; + uncompressed_in.open(uncompressed_path); + TEST_ASSERT_TRUE_MESSAGE(uncompressed_in.is_open(), + "Failed to open uncompressed file."); + + char *uncompressed = new char[BUFFER_SIZE]; + char *decompressed = new char[BUFFER_SIZE]; + while (uncompressed_in.tellg() < FILE_SIZE) { + const size_t read = + uncompressed_in.read(uncompressed, BUFFER_SIZE).gcount(); + const size_t pos = uncompressed_in.tellg(); + + TEST_ASSERT_EQUAL_UINT_MESSAGE(read, + unzip.decompress((uint8_t* ) decompressed, BUFFER_SIZE), + "The number of decompressed bytes didn't match the decompressed size."); + if (pos < FILE_SIZE) { + TEST_ASSERT_FALSE_MESSAGE(unzip.done(), + "The decompression was considered done before decompressing everything."); + TEST_ASSERT_EQUAL_INT32_MESSAGE(-1, unzip.getDecompressedSize(), + "The initial decompressed size didn't match expectations."); + } else { + TEST_ASSERT_TRUE_MESSAGE(unzip.done(), + "The decompression wasn't considered done after decompressing everything."); + TEST_ASSERT_EQUAL_INT32_MESSAGE(FILE_SIZE, + unzip.getDecompressedSize(), + "The final decompressed size didn't match expectations."); + } + + // Compare content in blocks of TEST_COMP_SIZE characters, for better error readability. + const size_t FILE_SIZE_LEN = log10(FILE_SIZE) + 1; + for (size_t start = 0; start + TEST_COMP_SIZE < BUFFER_SIZE; start += + TEST_COMP_SIZE) { + const size_t comp_len = std::min(TEST_COMP_SIZE, + BUFFER_SIZE - start); + const size_t comp_start = start + pos; + char message[58 + FILE_SIZE_LEN * 2]; + snprintf(message, 58 + FILE_SIZE_LEN * 2, + "Decompressed file bytes %lu-%lu don't match uncompressed file.", + comp_start, comp_start + comp_len); + TEST_ASSERT_EQUAL_STRING_LEN_MESSAGE(uncompressed + start, + decompressed + start, comp_len, message); + } + } + + compressed_in->close(); + uncompressed_in.close(); +} + +void test_decompress_large_wsize() { + // The size of the uncompressed file used for testing. + const size_t FILE_SIZE = 524288; + // The size of the block of data to be compared when checking the file. + const size_t TEST_COMP_SIZE = 25; + + check_fixtures(); + + // Change window size from -10 to -15. + compress_command[58] = '5'; + prepare_compressed_file(FILE_SIZE); + + compressed_in = new std::ifstream(); + compressed_in->open(compressed_path, std::ios::in | std::ios::binary); + TEST_ASSERT_TRUE_MESSAGE(compressed_in->is_open(), + "Failed to open compressed file."); + char *compressed = new char[FILE_SIZE]; + compressed_length = compressed_in->read(compressed, FILE_SIZE).gcount(); + compressed_in->close(); + + gzip::uzlib_ungzip_wrapper unzip((uint8_t*) compressed, + (uint8_t*) compressed + compressed_length, -15); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(FILE_SIZE, unzip.getDecompressedSize(), + "The initial decompressed size didn't match expectations."); + std::ifstream uncompressed_in; + uncompressed_in.open(uncompressed_path); + TEST_ASSERT_TRUE_MESSAGE(uncompressed_in.is_open(), + "Failed to open uncompressed file."); + char *uncompressed = new char[FILE_SIZE]; + uncompressed_in.read(uncompressed, FILE_SIZE); + uncompressed_in.close(); + + char *decompressed = new char[FILE_SIZE]; + TEST_ASSERT_EQUAL_UINT_MESSAGE(FILE_SIZE, + unzip.decompress((uint8_t* ) decompressed, FILE_SIZE), + "The number of decompressed bytes didn't match the decompressed size."); + TEST_ASSERT_TRUE_MESSAGE(unzip.done(), + "The decompression wasn't considered done after decompressing everything."); + TEST_ASSERT_EQUAL_INT32_MESSAGE(FILE_SIZE, unzip.getDecompressedSize(), + "The final decompressed size didn't match expectations."); + + // Compare content in blocks of TEST_COMP_SIZE characters, for better error readability. + const size_t FILE_SIZE_LEN = log10(FILE_SIZE) + 1; + for (size_t start = 0; start + TEST_COMP_SIZE < FILE_SIZE; start += + TEST_COMP_SIZE) { + const size_t comp_len = std::min(TEST_COMP_SIZE, FILE_SIZE - start); + char message[58 + FILE_SIZE_LEN * 2]; + snprintf(message, 58 + FILE_SIZE_LEN * 2, + "Decompressed file bytes %lu-%lu don't match uncompressed file.", + start, start + comp_len); + TEST_ASSERT_EQUAL_STRING_LEN_MESSAGE(uncompressed + start, + decompressed + start, comp_len, message); + } +} + +/** + * The entrypoint running this test file. + * + * @param argc The number of arguments. + * @param argv The given argument strings. + * @return The program exit code. + */ +int main(int argc, char **argv) { + UNITY_BEGIN(); + + RUN_TEST(test_decompress_small); + RUN_TEST(test_decompress_large); + RUN_TEST(test_decompress_streaming); + RUN_TEST(test_decompress_large_wsize); + + return UNITY_END(); +}