From 6b34dc93b85c0de5bd2cff671fe8150f36d6d99c Mon Sep 17 00:00:00 2001 From: Philip Salzmann Date: Tue, 7 May 2024 21:39:30 +0200 Subject: [PATCH 1/6] Add synchronization feature --- CMakeLists.txt | 4 +- README.md | 6 + simple_sync_server.py | 32 ++ src/address_book.cpp | 24 ++ src/address_book.hpp | 5 + src/main.cpp | 11 +- src/synchronization.cpp | 91 +++++ src/synchronization.hpp | 35 ++ vendor/http.h | 719 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 924 insertions(+), 3 deletions(-) create mode 100755 simple_sync_server.py create mode 100644 src/synchronization.cpp create mode 100644 src/synchronization.hpp create mode 100644 vendor/http.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f077c5..ee1f7f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,9 +17,9 @@ endmacro() fetch_content_from_submodule(Catch2 vendor/Catch2) -add_executable(address_book src/main.cpp src/address_book.cpp) +add_executable(address_book src/main.cpp src/address_book.cpp src/synchronization.cpp) target_include_directories(address_book PRIVATE vendor) -add_executable(address_book_tests test/address_book_tests.cpp src/address_book.cpp) +add_executable(address_book_tests test/address_book_tests.cpp src/address_book.cpp src/synchronization.cpp) target_include_directories(address_book_tests PRIVATE src vendor) target_link_libraries(address_book_tests Catch2::Catch2WithMain) diff --git a/README.md b/README.md index 155b918..0d1b0fa 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,14 @@ This is a small example application for which we want to write unit tests using To build and run the application and tests, the following are required: - CMake - A modern C++ compiler (a few C++20 features are used, GCC 11.4 is sufficient) +- Python 3 (to run the HTTP synchronization server) Configure the build using `cmake -S . -B build`, then use `cmake --build build` to compile. This produces two executables, `address_book` and `address_book_tests` inside the `build` directory. Both executables can be called without any arguments; the tests support various options for how to run them, run `address_book_tests --help` for more information. +To enable the synchronization feature, start the `simple_sync_server.py`. + ## Specification The `address_book` class defined in `src/address_book.hpp|.cpp` adheres to the following specification: @@ -25,3 +28,6 @@ The `address_book` class defined in `src/address_book.hpp|.cpp` adheres to the f - A phone number and birthday can be set for each entry. - Attempting to set a phone number or birthday on a non-existent entry throws an exception. - Attempting to set an invalid date as birthday throws an exception. +- The address book can be synchronized with a remote location using a *synchronization provider*. + - The synchronization provider offers a single function `synchronize`, that is given a list of all local entries, serialized as `name,number,MM/DD`, for example `jane m doe,123456789,11/30`. + It then returns a list in the same format after synchronization. diff --git a/simple_sync_server.py b/simple_sync_server.py new file mode 100755 index 0000000..2c294e2 --- /dev/null +++ b/simple_sync_server.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +import http.server +import socketserver + +PORT = 3333 + +address_book_data = b'' + +class AddressBookRequestHandler(http.server.BaseHTTPRequestHandler): + def __init__(self, request, client_address, server): + super().__init__(request, client_address, server) + + def do_GET(self): + if self.path == '/address-book': + self.send_response(200) + self.send_header('Content-type', 'application/text') + self.end_headers() + global address_book_data + self.wfile.write(address_book_data) + + def do_POST(self): + if self.path == '/address-book': + self.send_response(200) + self.send_header('Content-type', 'application/text') + self.end_headers() + content_length = int(self.headers['Content-Length']) + global address_book_data + address_book_data = self.rfile.read(content_length) + +with socketserver.TCPServer(("", PORT), AddressBookRequestHandler) as httpd: + print("Serving on port", PORT) + httpd.serve_forever() \ No newline at end of file diff --git a/src/address_book.cpp b/src/address_book.cpp index 7b59a57..8c98548 100644 --- a/src/address_book.cpp +++ b/src/address_book.cpp @@ -83,6 +83,30 @@ std::string address_book::get_next_birthday() const { return prettify_name(name); } +void address_book::synchronize(synchronization_provider& provider) { + std::vector serialized_entries; + for(const auto& [name, entry] : m_entries) { + std::string serialized_entry = name + "," + std::to_string(entry.phone_number) + "," + + std::to_string(static_cast(entry.birthday.month())) + "/" + + std::to_string(static_cast(entry.birthday.day())); + serialized_entries.push_back(serialized_entry); + } + std::vector merged_entries = provider.synchronize(serialized_entries); + m_entries.clear(); + for(const std::string& serialized_entry : merged_entries) { + std::string name(max_name_length, '\0'); + std::uint64_t phone_number; + unsigned month; + unsigned day; + char format[32] = {0}; + snprintf(format, sizeof(format), "%%%zu[^,],%%lu,%%u/%%u", max_name_length); + sscanf(serialized_entry.c_str(), format, name.data(), &phone_number, &month, &day); + name.erase(name.find('\0')); + m_entries.try_emplace(normalize_name(name), + entry{phone_number, std::chrono::month_day{std::chrono::month{month}, std::chrono::day{day}}}); + } +} + address_book::entry& address_book::get_entry(std::string name) { auto it = m_entries.find(normalize_name(name)); if(it == m_entries.end()) { throw std::invalid_argument("Entry not found"); } diff --git a/src/address_book.hpp b/src/address_book.hpp index d621489..8f3677d 100644 --- a/src/address_book.hpp +++ b/src/address_book.hpp @@ -1,5 +1,7 @@ #pragma once +#include "synchronization.hpp" + #include #include #include @@ -46,6 +48,9 @@ class address_book { /// Throws if the address book is empty. std::string get_next_birthday() const; + /// Synchronizes the address book with a remote provider. + void synchronize(synchronization_provider& provider); + private: struct entry { std::uint64_t phone_number; diff --git a/src/main.cpp b/src/main.cpp index 2b7c2f3..4f52cc3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -52,12 +52,18 @@ void next_birthday(address_book& ab) { printf("%s's birthday is on %u/%u\n", name.c_str(), (unsigned)birthday.month(), (unsigned)birthday.day()); } +void synchronize(address_book& ab) { + http_synchronization_provider provider{"http://localhost:3333"}; + ab.synchronize(provider); + printf("Synchronized %zu entries\n", ab.get_entries().size()); +} + int main() { address_book ab; printf("Address Book\n"); while(true) { - printf("\n(A)dd entry, (R)emove entry, (L)ist entries, (N)ext birthday, (Q)uit\n"); + printf("\n(A)dd entry, (R)emove entry, (L)ist entries, (N)ext birthday, (S)ynchronize, (Q)uit\n"); try { char choice; @@ -76,6 +82,9 @@ int main() { case 'N': case 'n': next_birthday(ab); break; + case 'S': + case 's': synchronize(ab); break; + case 'Q': case 'q': return 0; diff --git a/src/synchronization.cpp b/src/synchronization.cpp new file mode 100644 index 0000000..24121c7 --- /dev/null +++ b/src/synchronization.cpp @@ -0,0 +1,91 @@ +#include "synchronization.hpp" + +#define HTTP_IMPLEMENTATION +#include + +#include +#include +#include + +std::vector synchronization_provider::merge_entries( + std::vector local_entries, std::vector remote_entries) { + std::vector merged_entries = local_entries; + for(std::string remote_entry : remote_entries) { + bool found = false; + std::string name = remote_entry.substr(0, remote_entry.find(',')); + if(std::none_of(merged_entries.begin(), merged_entries.end(), + [&name](std::string entry) { return entry.substr(0, entry.find(',')) == name; })) { + merged_entries.push_back(remote_entry); + } + } + return merged_entries; +} + +http_synchronization_provider::http_synchronization_provider(std::string url) : m_url(url) {} + +void await_request(http_t* request) { + http_status_t status = HTTP_STATUS_PENDING; + while(status == HTTP_STATUS_PENDING) { + status = http_process(request); + } + if(status == HTTP_STATUS_FAILED) { + printf("HTTP request failed (%d): %s.\n", request->status_code, request->reason_phrase); + http_release(request); + throw std::runtime_error("HTTP request failed"); + } +} + +std::vector http_synchronization_provider::synchronize(std::vector serialized_entries) { + http_t* get_request = http_get((m_url + "/address-book").c_str(), NULL); + if(!get_request) { throw std::runtime_error("Invalid request"); } + await_request(get_request); + std::string response((const char*)get_request->response_data); + http_release(get_request); + + std::vector remote_entries; + std::stringstream ss{response}; + std::string line; + while(std::getline(ss, line)) { + remote_entries.push_back(line); + } + + std::vector merged_entries = merge_entries(serialized_entries, remote_entries); + + std::string merged_entries_str; + for(std::string entry : merged_entries) { + merged_entries_str += entry + '\n'; + } + + http_t* post_request = + http_post((m_url + "/address-book").c_str(), merged_entries_str.c_str(), merged_entries_str.size(), NULL); + if(!post_request) { throw std::runtime_error("Invalid request"); } + await_request(post_request); + http_release(post_request); + + return merged_entries; +} + +file_synchronization_provider::file_synchronization_provider(std::string path) : m_path(path) {} + +std::vector file_synchronization_provider::synchronize(std::vector serialized_entries) { + std::vector file_entries; + { + std::fstream file{m_path, std::ios::in | std::ios::binary}; + if(file.is_open()) { + std::string line; + while(std::getline(file, line)) { + file_entries.push_back(line); + } + } + } + std::vector merged_entries = merge_entries(serialized_entries, file_entries); + { + std::fstream file{m_path, std::ios::out | std::ios::binary | std::ios::trunc}; + if(!file.is_open()) { throw std::runtime_error("Could not open file " + m_path); } + for(std::string entry : merged_entries) { + file << entry << '\n'; + } + } + + return merged_entries; +} \ No newline at end of file diff --git a/src/synchronization.hpp b/src/synchronization.hpp new file mode 100644 index 0000000..d84f90f --- /dev/null +++ b/src/synchronization.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +/// Abstract base class for synchronization providers. +class synchronization_provider { + public: + virtual std::vector synchronize(std::vector serialized_entries) = 0; + virtual ~synchronization_provider() = default; + + protected: + std::vector merge_entries( + std::vector local_entries, std::vector remote_entries); +}; + +// Synchronization provider that synchronizes with a HTTP server. +class http_synchronization_provider : public synchronization_provider { + public: + http_synchronization_provider(std::string url); + std::vector synchronize(std::vector serialized_entries) override; + + private: + std::string m_url; +}; + +// Synchronization provider that synchronizes with a file. +class file_synchronization_provider : public synchronization_provider { + public: + file_synchronization_provider(std::string path); + std::vector synchronize(std::vector serialized_entries) override; + + private: + std::string m_path; +}; \ No newline at end of file diff --git a/vendor/http.h b/vendor/http.h new file mode 100644 index 0000000..9d6f9dd --- /dev/null +++ b/vendor/http.h @@ -0,0 +1,719 @@ +/* +------------------------------------------------------------------------------ + Licensing information can be found at the end of the file. +------------------------------------------------------------------------------ + +http.hpp - v1.0 - Basic HTTP protocol implementation over sockets (no https). + +Do this: + #define HTTP_IMPLEMENTATION +before you include this file in *one* C/C++ file to create the implementation. +*/ + +#ifndef http_hpp +#define http_hpp + +#define _CRT_NONSTDC_NO_DEPRECATE +#define _CRT_SECURE_NO_WARNINGS +#include // for size_t +#include // for uintptr_t + +typedef enum http_status_t + { + HTTP_STATUS_PENDING, + HTTP_STATUS_COMPLETED, + HTTP_STATUS_FAILED, + } http_status_t; + +typedef struct http_t + { + http_status_t status; + int status_code; + char const* reason_phrase; + char const* content_type; + size_t response_size; + void* response_data; + } http_t; + +http_t* http_get( char const* url, void* memctx ); +http_t* http_post( char const* url, void const* data, size_t size, void* memctx ); + +http_status_t http_process( http_t* http ); + +void http_release( http_t* http ); + +#endif /* http_hpp */ + +/** + +http.hpp +======== + +Basic HTTP protocol implementation over sockets (no https). + + +Example +------- + + #define HTTP_IMPLEMENTATION + #include "http.h" + + int main( int argc, char** argv ) { + http_t* request = http_get( "http://www.mattiasgustavsson.com/http_test.txt", NULL ); + if( !request ) { + printf( "Invalid request.\n" ); + return 1; + } + + http_status_t status = HTTP_STATUS_PENDING; + int prev_size = -1; + while( status == HTTP_STATUS_PENDING ) { + status = http_process( request ); + if( prev_size != (int) request->response_size ) { + printf( "%d byte(s) received.\n", (int) request->response_size ); + prev_size = (int) request->response_size; + } + } + + if( status == HTTP_STATUS_FAILED ) { + printf( "HTTP request failed (%d): %s.\n", request->status_code, request->reason_phrase ); + http_release( request ); + return 1; + } + + printf( "\nContent type: %s\n\n%s\n", request->content_type, (char const*)request->response_data ); + http_release( request ); + return 0; + } + + +API Documentation +----------------- + +http.h is a small library for making http requests from a web server. It only supports GET and POST http commands, and +is designed for when you just need a very basic way of communicating over http. http.h does not support https +connections, just plain http. + +http.h is a single-header library, and does not need any .lib files or other binaries, or any build scripts. To use +it, you just include http.h to get the API declarations. To get the definitions, you must include http.h from +*one* single C or C++ file, and #define the symbol `HTTP_IMPLEMENTATION` before you do. + + +#### Custom memory allocators + +For working memory and to store the retrieved data, http.h needs to do dynamic allocation by calling `malloc`. Programs +might want to keep track of allocations done, or use custom defined pools to allocate memory from. http.h allows +for specifying custom memory allocation functions for `malloc` and `free`. This is done with the following code: + + #define HTTP_IMPLEMENTATION + #define HTTP_MALLOC( ctx, size ) ( my_custom_malloc( ctx, size ) ) + #define HTTP_FREE( ctx, ptr ) ( my_custom_free( ctx, ptr ) ) + #include "http.h" + +where `my_custom_malloc` and `my_custom_free` are your own memory allocation/deallocation functions. The `ctx` parameter +is an optional parameter of type `void*`. When `http_get` or `http_post` is called, , you can pass in a `memctx` +parameter, which can be a pointer to anything you like, and which will be passed through as the `ctx` parameter to every +`HTTP_MALLOC`/`HTTP_FREE` call. For example, if you are doing memory tracking, you can pass a pointer to your +tracking data as `memctx`, and in your custom allocation/deallocation function, you can cast the `ctx` param back to the +right type, and access the tracking data. + +If no custom allocator is defined, http.h will default to `malloc` and `free` from the C runtime library. + + +http_get +-------- + + http_t* http_get( char const* url, void* memctx ) + +Initiates a http GET request with the specified url. `url` is a zero terminated string containing the request location, +just like you would type it in a browser, for example `http://www.mattiasgustavsson.com:80/http_test.txt`. `memctx` is a +pointer to user defined data which will be passed through to the custom HTTP_MALLOC/HTTP_FREE calls. It can be NULL if +no user defined data is needed. Returns a `http_t` instance, which needs to be passed to `http_process` to process the +request. When the request is finished (or have failed), the returned `http_t` instance needs to be released by calling +`http_release`. If the request was invalid, `http_get` returns NULL. + + +http_post +--------- + + http_t* http_post( char const* url, void const* data, size_t size, void* memctx ) + +Initiates a http POST request with the specified url. `url` is a zero terminated string containing the request location, +just like you would type it in a browser, for example `http://www.mattiasgustavsson.com:80/http_test.txt`. `data` is a +pointer to the data to be sent along as part of the request, and `size` is the number of bytes to send. `memctx` is a +pointer to user defined data which will be passed through to the custom HTTP_MALLOC/HTTP_FREE calls. It can be NULL if +no user defined data is needed. Returns a `http_t` instance, which needs to be passed to `http_process` to process the +request. When the request is finished (or have failed), the returned `http_t` instance needs to be released by calling +`http_release`. If the request was invalid, `http_post` returns NULL. + + +http_process +------------ + + http_status_t http_process( http_t* http ) + +http.h uses non-blocking sockets, so after a request have been made by calling either `http_get` or `http_post`, you +have to keep calling `http_process` for as long as it returns `HTTP_STATUS_PENDING`. You can call it from a loop which +does other work too, for example from inside a game loop or from a loop which calls `http_process` on multiple requests. +If the request fails, `http_process` returns `HTTP_STATUS_FAILED`, and the fields `status_code` and `reason_phrase` may +contain more details (for example, status code can be 404 if the requested resource was not found on the server). If the +request completes successfully, it returns `HTTP_STATUS_COMPLETED`. In this case, the `http_t` instance will contain +details about the result. `status_code` and `reason_phrase` contains the details about the result, as specified in the +HTTP protocol. `content_type` contains the MIME type for the returns resource, for example `text/html` for a normal web +page. `response_data` is the pointer to the received data, and `resonse_size` is the number of bytes it contains. In the +case when the response data is in text format, http.h ensures there is a zero terminator placed immediately after the +response data block, so it is safe to interpret the resonse data as a `char*`. Note that the data size in this case will +be the length of the data without the additional zero terminator. + + +http_release +------------ + + void http_release( http_t* http ) + +Releases the resources acquired by `http_get` or `http_post`. Should be call when you are finished with the request. + +*/ + +/* +---------------------- + IMPLEMENTATION +---------------------- +*/ + +#ifdef HTTP_IMPLEMENTATION + +#ifdef _WIN32 + #define _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_SECURE_NO_WARNINGS + #pragma warning( push ) + #pragma warning( disable: 4127 ) // conditional expression is constant + #pragma warning( disable: 4255 ) // 'function' : no function prototype given: converting '()' to '(void)' + #pragma warning( disable: 4365 ) // 'action' : conversion from 'type_1' to 'type_2', signed/unsigned mismatch + #pragma warning( disable: 4574 ) // 'Identifier' is defined to be '0': did you mean to use '#if identifier'? + #pragma warning( disable: 4668 ) // 'symbol' is not defined as a preprocessor macro, replacing with '0' for 'directive' + #pragma warning( disable: 4706 ) // assignment within conditional expression + #include + #include + #pragma warning( pop ) + #pragma comment (lib, "Ws2_32.lib") + #include + #include + #define HTTP_SOCKET SOCKET + #define HTTP_INVALID_SOCKET INVALID_SOCKET +#else + #include + #include + #include + #include + #include + #include + #include + #include + #include + #define HTTP_SOCKET int + #define HTTP_INVALID_SOCKET -1 +#endif + +#ifndef HTTP_MALLOC + #define _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_SECURE_NO_WARNINGS + #include + #define HTTP_MALLOC( ctx, size ) ( malloc( size ) ) + #define HTTP_FREE( ctx, ptr ) ( free( ptr ) ) +#endif + +typedef struct http_internal_t + { + /* keep this at the top!*/ + http_t http; + /* because http_internal_t* can be cast to http_t*. */ + + void* memctx; + HTTP_SOCKET socket; + int connect_pending; + int request_sent; + char address[ 256 ]; + char request_header[ 256 ]; + char* request_header_large; + void* request_data; + size_t request_data_size; + char reason_phrase[ 1024 ]; + char content_type[ 256 ]; + size_t data_size; + size_t data_capacity; + void* data; + } http_internal_t; + + +static int http_internal_parse_url( char const* url, char* address, size_t address_capacity, char* port, + size_t port_capacity, char const** resource ) + { + // make sure url starts with http:// + if( strncmp( url, "http://", 7 ) != 0 ) return 0; + url += 7; // skip http:// part of url + + size_t url_len = strlen( url ); + + // find end of address part of url + char const* address_end = strchr( url, ':' ); + if( !address_end ) address_end = strchr( url, '/' ); + if( !address_end ) address_end = url + url_len; + + // extract address + size_t address_len = (size_t)( address_end - url ); + if( address_len >= address_capacity ) return 0; + memcpy( address, url, address_len ); + address[ address_len ] = 0; + + // check if there's a port defined + char const* port_end = address_end; + if( *address_end == ':' ) + { + ++address_end; + port_end = strchr( address_end, '/' ); + if( !port_end ) port_end = address_end + strlen( address_end ); + size_t port_len = (size_t)( port_end - address_end ); + if( port_len >= port_capacity ) return 0; + memcpy( port, address_end, port_len ); + port[ port_len ] = 0; + } + else + { + // use default port number 80 + if( port_capacity <= 2 ) return 0; + strcpy( port, "80" ); + } + + + *resource = port_end; + + return 1; + } + + +HTTP_SOCKET http_internal_connect( char const* address, char const* port ) + { + // set up hints for getaddrinfo + struct addrinfo hints; + memset( &hints, 0, sizeof( hints ) ); + hints.ai_family = AF_UNSPEC; // the Internet Protocol version 4 (IPv4) address family. + hints.ai_flags = AI_PASSIVE; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; // Use Transmission Control Protocol (TCP). + + // resolve the server address and port + struct addrinfo* addri = 0; + int error = getaddrinfo( address, port, &hints, &addri) ; + if( error != 0 ) return HTTP_INVALID_SOCKET; + + // create the socket + HTTP_SOCKET sock = socket( addri->ai_family, addri->ai_socktype, addri->ai_protocol ); + if( sock == -1) + { + freeaddrinfo( addri ); + return HTTP_INVALID_SOCKET; + } + + // set socket to nonblocking mode + u_long nonblocking = 1; + #ifdef _WIN32 + int res = ioctlsocket( sock, FIONBIO, &nonblocking ); + #else + int flags = fcntl( sock, F_GETFL, 0 ); + int res = fcntl( sock, F_SETFL, flags | O_NONBLOCK ); + #endif + if( res == -1 ) + { + freeaddrinfo( addri ); + #ifdef _WIN32 + closesocket( sock ); + #else + close( sock ); + #endif + return HTTP_INVALID_SOCKET; + } + + // connect to server + if( connect( sock, addri->ai_addr, (int)addri->ai_addrlen ) == -1 ) + { + #ifdef _WIN32 + if( WSAGetLastError() != WSAEWOULDBLOCK && WSAGetLastError() != WSAEINPROGRESS ) + { + freeaddrinfo( addri ); + closesocket( sock ); + return HTTP_INVALID_SOCKET; + } + #else + if( errno != EWOULDBLOCK && errno != EINPROGRESS && errno != EAGAIN ) + { + freeaddrinfo( addri ); + close( sock ); + return HTTP_INVALID_SOCKET; + } + #endif + } + + freeaddrinfo( addri ); + return sock; + } + + +static http_internal_t* http_internal_create( size_t request_data_size, void* memctx ) + { + http_internal_t* internal = (http_internal_t*) HTTP_MALLOC( memctx, sizeof( http_internal_t ) + request_data_size ); + + internal->http.status = HTTP_STATUS_PENDING; + internal->http.status_code = 0; + internal->http.response_size = 0; + internal->http.response_data = NULL; + + internal->memctx = memctx; + internal->connect_pending = 1; + internal->request_sent = 0; + + strcpy( internal->reason_phrase, "" ); + internal->http.reason_phrase = internal->reason_phrase; + + strcpy( internal->content_type, "" ); + internal->http.content_type = internal->content_type; + + internal->data_size = 0; + internal->data_capacity = 64 * 1024; + internal->data = HTTP_MALLOC( memctx, internal->data_capacity ); + + internal->request_data = NULL; + internal->request_data_size = 0; + + return internal; + } + + +http_t* http_get( char const* url, void* memctx ) + { + #ifdef _WIN32 + WSADATA wsa_data; + if( WSAStartup( MAKEWORD( 1, 0 ), &wsa_data ) != 0 ) return NULL; + #endif + + char address[ 256 ]; + char port[ 16 ]; + char const* resource; + + if( http_internal_parse_url( url, address, sizeof( address ), port, sizeof( port ), &resource ) == 0 ) + return NULL; + + HTTP_SOCKET socket = http_internal_connect( address, port ); + if( socket == HTTP_INVALID_SOCKET ) return NULL; + + http_internal_t* internal = http_internal_create( 0, memctx ); + internal->socket = socket; + + char* request_header; + size_t request_header_len = 64 + strlen( resource ) + strlen( address ) + strlen( port ); + if( request_header_len < sizeof( internal->request_header ) ) + { + internal->request_header_large = NULL; + request_header = internal->request_header; + } + else + { + internal->request_header_large = (char*) HTTP_MALLOC( memctx, request_header_len + 1 ); + request_header = internal->request_header_large; + } + int default_http_port = (strcmp(port, "80") == 0); + sprintf( request_header, "GET %s HTTP/1.0\r\nHost: %s%s%s\r\n\r\n", resource, address, default_http_port ? "" : ":", default_http_port ? "" : port ); + + return &internal->http; + } + + +http_t* http_post( char const* url, void const* data, size_t size, void* memctx ) + { + #ifdef _WIN32 + WSADATA wsa_data; + if( WSAStartup( MAKEWORD( 1, 0 ), &wsa_data ) != 0 ) return 0; + #endif + + char address[ 256 ]; + char port[ 16 ]; + char const* resource; + + if( http_internal_parse_url( url, address, sizeof( address ), port, sizeof( port ), &resource ) == 0 ) + return NULL; + + HTTP_SOCKET socket = http_internal_connect( address, port ); + if( socket == HTTP_INVALID_SOCKET ) return NULL; + + http_internal_t* internal = http_internal_create( size, memctx ); + internal->socket = socket; + + char* request_header; + size_t request_header_len = 64 + strlen( resource ) + strlen( address ) + strlen( port ); + if( request_header_len < sizeof( internal->request_header ) ) + { + internal->request_header_large = NULL; + request_header = internal->request_header; + } + else + { + internal->request_header_large = (char*) HTTP_MALLOC( memctx, request_header_len + 1 ); + request_header = internal->request_header_large; + } + int default_http_port = (strcmp(port, "80") == 0); + sprintf( request_header, "POST %s HTTP/1.0\r\nHost: %s%s%s\r\nContent-Length: %d\r\n\r\n", resource, address, default_http_port ? "" : ":", default_http_port ? "" : port, + (int) size ); + + internal->request_data_size = size; + internal->request_data = ( internal + 1 ); + memcpy( internal->request_data, data, size ); + + return &internal->http; + } + + +http_status_t http_process( http_t* http ) + { + http_internal_t* internal = (http_internal_t*) http; + + if( http->status == HTTP_STATUS_FAILED ) return http->status; + + if( internal->connect_pending ) + { + fd_set sockets_to_check; + FD_ZERO( &sockets_to_check ); + #pragma warning( push ) + #pragma warning( disable: 4548 ) // expression before comma has no effect; expected expression with side-effect + FD_SET( internal->socket, &sockets_to_check ); + #pragma warning( pop ) + struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; + // check if socket is ready for send + if( select( (int)( internal->socket + 1 ), NULL, &sockets_to_check, NULL, &timeout ) == 1 ) + { + int opt = -1; + socklen_t len = sizeof( opt ); + if( getsockopt( internal->socket, SOL_SOCKET, SO_ERROR, (char*)( &opt ), &len) >= 0 && opt == 0 ) + internal->connect_pending = 0; // if it is, we're connected + } + } + + if( internal->connect_pending ) return http->status; + + if( !internal->request_sent ) + { + char const* request_header = internal->request_header_large ? + internal->request_header_large : internal->request_header; + if( send( internal->socket, request_header, (int) strlen( request_header ), 0 ) == -1 ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + if( internal->request_data_size ) + { + int res = send( internal->socket, (char const*)internal->request_data, (int) internal->request_data_size, 0 ); + if( res == -1 ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + } + internal->request_sent = 1; + return http->status; + } + + // check if socket is ready for recv + fd_set sockets_to_check; + FD_ZERO( &sockets_to_check ); + #pragma warning( push ) + #pragma warning( disable: 4548 ) // expression before comma has no effect; expected expression with side-effect + FD_SET( internal->socket, &sockets_to_check ); + #pragma warning( pop ) + struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; + while( select( (int)( internal->socket + 1 ), &sockets_to_check, NULL, NULL, &timeout ) == 1 ) + { + char buffer[ 4096 ]; + int size = recv( internal->socket, buffer, sizeof( buffer ), 0 ); + if( size == -1 ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + else if( size > 0 ) + { + size_t min_size = internal->data_size + size + 1; + if( internal->data_capacity < min_size ) + { + internal->data_capacity *= 2; + if( internal->data_capacity < min_size ) internal->data_capacity = min_size; + void* new_data = HTTP_MALLOC( memctx, internal->data_capacity ); + memcpy( new_data, internal->data, internal->data_size ); + HTTP_FREE( memctx, internal->data ); + internal->data = new_data; + } + memcpy( (void*)( ( (uintptr_t) internal->data ) + internal->data_size ), buffer, (size_t) size ); + internal->data_size += size; + } + else if( size == 0 ) + { + char const* status_line = (char const*) internal->data; + + int header_size = 0; + char const* header_end = strstr( status_line, "\r\n\r\n" ); + if( header_end ) + { + header_end += 4; + header_size = (int)( header_end - status_line ); + } + else + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + + // skip http version + status_line = strchr( status_line, ' ' ); + if( !status_line ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + ++status_line; + + // extract status code + char status_code[ 16 ]; + char const* status_code_end = strchr( status_line, ' ' ); + if( !status_code_end ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + memcpy( status_code, status_line, (size_t)( status_code_end - status_line ) ); + status_code[ status_code_end - status_line ] = 0; + status_line = status_code_end + 1; + http->status_code = atoi( status_code ); + + // extract reason phrase + char const* reason_phrase_end = strstr( status_line, "\r\n" ); + if( !reason_phrase_end ) + { + http->status = HTTP_STATUS_FAILED; + return http->status; + } + size_t reason_phrase_len = (size_t)( reason_phrase_end - status_line ); + if( reason_phrase_len >= sizeof( internal->reason_phrase ) ) + reason_phrase_len = sizeof( internal->reason_phrase ) - 1; + memcpy( internal->reason_phrase, status_line, reason_phrase_len ); + internal->reason_phrase[ reason_phrase_len ] = 0; + status_line = reason_phrase_end + 1; + + // extract content type + char const* content_type_start = strstr( status_line, "Content-Type: " ); + if( content_type_start ) + { + content_type_start += strlen( "Content-Type: " ); + char const* content_type_end = strstr( content_type_start, "\r\n" ); + if( content_type_end ) + { + size_t content_type_len = (size_t)( content_type_end - content_type_start ); + if( content_type_len >= sizeof( internal->content_type ) ) + content_type_len = sizeof( internal->content_type ) - 1; + memcpy( internal->content_type, content_type_start, content_type_len ); + internal->content_type[ content_type_len ] = 0; + } + } + + http->status = http->status_code < 300 ? HTTP_STATUS_COMPLETED : HTTP_STATUS_FAILED; + http->response_data = (void*)( ( (uintptr_t) internal->data ) + header_size ); + http->response_size = internal->data_size - header_size; + + // add an extra zero after the received data, but don't modify the size, so ascii results can be used as + // a zero terminated string. the size returned will be the string without this extra zero terminator. + ( (char*)http->response_data )[ http->response_size ] = 0; + return http->status; + } + } + + return http->status; + } + + +void http_release( http_t* http ) + { + http_internal_t* internal = (http_internal_t*) http; + #ifdef _WIN32 + closesocket( internal->socket ); + #else + close( internal->socket ); + #endif + + if( internal->request_header_large) HTTP_FREE( memctx, internal->request_header_large ); + HTTP_FREE( memctx, internal->data ); + HTTP_FREE( memctx, internal ); + #ifdef _WIN32 + WSACleanup(); + #endif + } + + +#endif /* HTTP_IMPLEMENTATION */ + +/* +revision history: + 1.0 first released version +*/ + +/* +------------------------------------------------------------------------------ + +This software is available under 2 licenses - you may choose the one you like. + +------------------------------------------------------------------------------ + +ALTERNATIVE A - MIT License + +Copyright (c) 2016 Mattias Gustavsson + +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, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +------------------------------------------------------------------------------ + +ALTERNATIVE B - Public Domain (www.unlicense.org) + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +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 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. + +------------------------------------------------------------------------------ +*/ From 6f26f2d355bc3fdfd3b7ce6a75ad1848786d2607 Mon Sep 17 00:00:00 2001 From: himaren Date: Wed, 8 May 2024 09:40:18 +0000 Subject: [PATCH 2/6] add tests --- test/address_book_tests.cpp | 75 ++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/test/address_book_tests.cpp b/test/address_book_tests.cpp index 8eb7209..9cdb892 100644 --- a/test/address_book_tests.cpp +++ b/test/address_book_tests.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "address_book.hpp" @@ -10,4 +11,76 @@ TEST_CASE("entries can be added and removed") { CHECK(ab.has_entry("Jane Doe")); ab.remove_entry("Jane Doe"); CHECK_FALSE(ab.has_entry("Jane Doe")); -} \ No newline at end of file +} + +TEST_CASE("entries are identified by their names") { + address_book ab; + ab.add_entry("name1"); + CHECK(ab.has_entry("name1")); + ab.add_entry("name1"); + + CHECK(ab.get_entries().size() == 1); +} + + +TEST_CASE("names are case-insensitive") { + address_book ab; + ab.add_entry("kleiner name"); + CHECK(ab.has_entry("kleiner name")); + CHECK(ab.has_entry("kleiner Name")); +} + +TEST_CASE("names must be between 1 and 100 characters long") { + address_book ab; + CHECK_THROWS(ab.add_entry("asdflöasdfkjashfoihasdoifnskfndjabsdfioasofdnasölfdknsödfhsoaifhnaläskdfnjksdfoiashüfdopinsalökfdnsaidfihjüaspofnkasöfnüaishdfanfdöasdflöasdfkjashfoihasdoifnskfndjabsdfioasofdnasölfdknsödfhsoaifhnaläskdfnjksdfoiashüfdopinsalökfdnsaidfihjüaspofnkasöfnüaishdfanfdö")); + CHECK_THROWS(ab.add_entry("")); +} + +TEST_CASE("get_entries returns all entries ordered alphabetically (capital letter)") { + address_book ab; + ab.add_entry("C"); + ab.add_entry("A"); + ab.add_entry("B"); + + auto entries = ab.get_entries(); + REQUIRE(entries.size() == 3); + CHECK(entries[0] == "A"); + CHECK(entries[1] == "B"); + CHECK(entries[2] == "C"); +} + +TEST_CASE("Names returned by the address book have the first letter of each word capitalized") { + address_book ab; + ab.add_entry("a"); + + auto entries = ab.get_entries(); + REQUIRE(entries.size() == 1); + CHECK(entries[0] == "A"); +} + +TEST_CASE("A phone number and birthday can be set for each entry.") { + address_book ab; + ab.add_entry("a"); + + ab.set_phone_number("a", 123); + +} + +TEST_CASE("Attempting to set a phone number or birthday on a non-existent entry throws an exception") { + address_book ab; + CHECK_THROWS(ab.set_phone_number("a", 123)); + + unsigned int month = 2; + unsigned int day = 3; + CHECK_THROWS(ab.set_birthday("a", std::chrono::month_day{std::chrono::month{month}, std::chrono::day{day}})); +} + +TEST_CASE("Attempting to set an invalid date as birthday throws an exception") { + address_book ab; + ab.add_entry("name"); + + unsigned int month = 14; + unsigned int day = 3; + CHECK_THROWS(ab.set_birthday("name", std::chrono::month_day{std::chrono::month{month}, std::chrono::day{day}})); +} + From bf3ffeb05c0cf36f282de655aeb2dfcb53c32bfc Mon Sep 17 00:00:00 2001 From: himaren Date: Wed, 8 May 2024 09:40:46 +0000 Subject: [PATCH 3/6] adapt workflows --- .github/workflows/hella_world.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hella_world.yml b/.github/workflows/hella_world.yml index 05f5eda..693ce53 100644 --- a/.github/workflows/hella_world.yml +++ b/.github/workflows/hella_world.yml @@ -4,9 +4,11 @@ on: push: branches: - main + - develop pull_request: branches: - main + - develop jobs: compile_and_run: @@ -16,7 +18,7 @@ jobs: uses: actions/checkout@v4 with: submodules: true - + - name: get-cmake uses: lukka/get-cmake@v3.29.3 From b89187d68f55814f12b4598122b5a6c8cfe89494 Mon Sep 17 00:00:00 2001 From: himaren Date: Wed, 8 May 2024 09:48:48 +0000 Subject: [PATCH 4/6] add exception output to test --- test/address_book_tests.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/address_book_tests.cpp b/test/address_book_tests.cpp index 9cdb892..c625c2f 100644 --- a/test/address_book_tests.cpp +++ b/test/address_book_tests.cpp @@ -30,10 +30,10 @@ TEST_CASE("names are case-insensitive") { CHECK(ab.has_entry("kleiner Name")); } -TEST_CASE("names must be between 1 and 100 characters long") { +TEST_CASE("Attempting to add an entry with a name violating these restrictions throws an exception") { address_book ab; - CHECK_THROWS(ab.add_entry("asdflöasdfkjashfoihasdoifnskfndjabsdfioasofdnasölfdknsödfhsoaifhnaläskdfnjksdfoiashüfdopinsalökfdnsaidfihjüaspofnkasöfnüaishdfanfdöasdflöasdfkjashfoihasdoifnskfndjabsdfioasofdnasölfdknsödfhsoaifhnaläskdfnjksdfoiashüfdopinsalökfdnsaidfihjüaspofnkasöfnüaishdfanfdö")); - CHECK_THROWS(ab.add_entry("")); + CHECK_THROWS_WITH(ab.add_entry("asdflöasdfkjashfoihasdoifnskfndjabsdfioasofdnasölfdknsödfhsoaifhnaläskdfnjksdfoiashüfdopinsalökfdnsaidfihjüaspofnkasöfnüaishdfanfdöasdflöasdfkjashfoihasdoifnskfndjabsdfioasofdnasölfdknsödfhsoaifhnaläskdfnjksdfoiashüfdopinsalökfdnsaidfihjüaspofnkasöfnüaishdfanfdö"), "Name too long"); + CHECK_THROWS_WITH(ab.add_entry(""), "Name may not be empty"); } TEST_CASE("get_entries returns all entries ordered alphabetically (capital letter)") { From c1471b2f8476c8835732eb5b01a7cbde27a6cc04 Mon Sep 17 00:00:00 2001 From: himaren Date: Wed, 8 May 2024 09:51:41 +0000 Subject: [PATCH 5/6] add more complex name to test --- test/address_book_tests.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/address_book_tests.cpp b/test/address_book_tests.cpp index c625c2f..5eb358f 100644 --- a/test/address_book_tests.cpp +++ b/test/address_book_tests.cpp @@ -13,7 +13,7 @@ TEST_CASE("entries can be added and removed") { CHECK_FALSE(ab.has_entry("Jane Doe")); } -TEST_CASE("entries are identified by their names") { +TEST_CASE("entries are identified by their names -> no second entry with same name can be added") { address_book ab; ab.add_entry("name1"); CHECK(ab.has_entry("name1")); @@ -51,11 +51,11 @@ TEST_CASE("get_entries returns all entries ordered alphabetically (capital lette TEST_CASE("Names returned by the address book have the first letter of each word capitalized") { address_book ab; - ab.add_entry("a"); + ab.add_entry("jane m doe"); auto entries = ab.get_entries(); REQUIRE(entries.size() == 1); - CHECK(entries[0] == "A"); + CHECK(entries[0] == "Jane M Doe"); } TEST_CASE("A phone number and birthday can be set for each entry.") { From 2e76b752851f2aed3f578072bc67576e1f7e1bfb Mon Sep 17 00:00:00 2001 From: himaren Date: Wed, 8 May 2024 11:39:19 +0000 Subject: [PATCH 6/6] add mock sync provider and test --- test/address_book_tests.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/address_book_tests.cpp b/test/address_book_tests.cpp index 5eb358f..8a5e03c 100644 --- a/test/address_book_tests.cpp +++ b/test/address_book_tests.cpp @@ -4,6 +4,31 @@ #include "address_book.hpp" +class mock_synchronization_provider : public synchronization_provider { + std::vector entries; + + public: + std::vector synchronize(std::vector serialized_entries) override { + entries = merge_entries(entries, serialized_entries); + return entries; + } + + + +}; + +TEST_CASE("test address_book::syncronize") { + address_book ab; + mock_synchronization_provider provider = mock_synchronization_provider(); + + ab.add_entry("Jane Doe"); + ab.synchronize(provider); + ab.remove_entry("Jane Doe"); + + ab.synchronize(provider); + CHECK(ab.has_entry("Jane Doe")); +} + TEST_CASE("entries can be added and removed") { address_book ab; CHECK_FALSE(ab.has_entry("Jane Doe"));