b1c3ec3af4
- Protocol definition with 6-byte header (chunk_idx: 2B, payload: N B, crc32: 4B) - Node.js server with chunk-based HTTP delivery - C client library with resumable download support - CRC32 implementation (IEEE 802.3) - Test suites for both server and CRC32 - 1024B default chunk size (0.59% overhead)
434 lines
13 KiB
C
434 lines
13 KiB
C
/**
|
|
* @file chunk_client.c
|
|
* @brief Chunk Transfer Client Implementation
|
|
*
|
|
* Note: This implementation requires an HTTP client library.
|
|
* For ESP32, use ESP-IDF's HTTP Client or Arduino's HTTPClient.
|
|
* For POSIX systems, use libcurl.
|
|
*/
|
|
|
|
#include "protocol.h"
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
/* HTTP client interface (to be implemented per platform) */
|
|
typedef struct {
|
|
/**
|
|
* Send HTTP GET request
|
|
* @param url Full URL
|
|
* @param response Buffer for response data
|
|
* @param response_len Buffer size
|
|
* @param actual_len Actual response length (output)
|
|
* @param status_code HTTP status code (output)
|
|
* @return 0 on success, error code otherwise
|
|
*/
|
|
int (*http_get)(const char *url, uint8_t *response, size_t response_len,
|
|
size_t *actual_len, int *status_code);
|
|
|
|
/**
|
|
* Get header value from last response
|
|
* @param name Header name
|
|
* @return Header value or NULL
|
|
*/
|
|
const char *(*http_get_header)(const char *name);
|
|
|
|
void *ctx; /* Platform-specific context */
|
|
} http_client_t;
|
|
|
|
/* Client state */
|
|
typedef struct {
|
|
chunk_client_config_t config;
|
|
http_client_t http;
|
|
|
|
char current_file[256];
|
|
uint32_t file_size;
|
|
uint16_t total_chunks;
|
|
uint16_t current_chunk;
|
|
chunk_state_t state;
|
|
chunk_err_t error_code;
|
|
bool cancel_requested;
|
|
} chunk_client_state_t;
|
|
|
|
static chunk_client_state_t client_state;
|
|
|
|
/**
|
|
* Initialize chunk client
|
|
*/
|
|
chunk_err_t chunk_client_init(const chunk_client_config_t *config) {
|
|
if (!config || !config->server_url) {
|
|
return CHUNK_ERR_INVALID_PARAM;
|
|
}
|
|
|
|
memset(&client_state, 0, sizeof(client_state));
|
|
memcpy(&client_state.config, config, sizeof(chunk_client_config_t));
|
|
|
|
/* Set default chunk size if not specified */
|
|
if (client_state.config.chunk_size == 0) {
|
|
client_state.config.chunk_size = CHUNK_DEFAULT_SIZE;
|
|
}
|
|
|
|
client_state.state = CHUNK_STATE_IDLE;
|
|
client_state.error_code = CHUNK_ERR_OK;
|
|
|
|
return CHUNK_ERR_OK;
|
|
}
|
|
|
|
/**
|
|
* Get file size from server
|
|
*/
|
|
static chunk_err_t get_file_size(const char *filename, uint32_t *size) {
|
|
char url[512];
|
|
snprintf(url, sizeof(url), "%s/ota?file=%s",
|
|
client_state.config.server_url, filename);
|
|
|
|
/* Send HEAD or GET request to get Content-Length */
|
|
uint8_t dummy[1];
|
|
size_t actual_len;
|
|
int status_code;
|
|
|
|
int ret = client_state.http.http_get(url, dummy, sizeof(dummy),
|
|
&actual_len, &status_code);
|
|
if (ret != 0) {
|
|
return CHUNK_ERR_NETWORK;
|
|
}
|
|
|
|
if (status_code != 200) {
|
|
return (status_code == 404) ? CHUNK_ERR_FILE_NOT_FOUND : CHUNK_ERR_SERVER_ERROR;
|
|
}
|
|
|
|
/* Parse Content-Length from headers */
|
|
const char *cl = client_state.http.http_get_header("X-File-Size");
|
|
if (!cl) {
|
|
cl = client_state.http.http_get_header("Content-Length");
|
|
}
|
|
|
|
if (cl) {
|
|
*size = (uint32_t)atoi(cl);
|
|
} else {
|
|
/* Fallback: estimate from first chunk */
|
|
*size = client_state.config.chunk_size;
|
|
}
|
|
|
|
return CHUNK_ERR_OK;
|
|
}
|
|
|
|
/**
|
|
* Download a single chunk
|
|
*/
|
|
static chunk_err_t download_chunk(uint16_t chunk_idx, uint8_t *buffer,
|
|
uint16_t *payload_len, uint32_t *crc32) {
|
|
char url[512];
|
|
snprintf(url, sizeof(url), "%s/ota?file=%s&chunk=%u",
|
|
client_state.config.server_url,
|
|
client_state.current_file, chunk_idx);
|
|
|
|
/* Allocate buffer for chunk response */
|
|
size_t response_size = 2 + client_state.config.chunk_size + 4;
|
|
uint8_t *response = (uint8_t *)malloc(response_size);
|
|
if (!response) {
|
|
return CHUNK_ERR_MEMORY;
|
|
}
|
|
|
|
size_t actual_len;
|
|
int status_code;
|
|
|
|
int ret = client_state.http.http_get(url, response, response_size,
|
|
&actual_len, &status_code);
|
|
|
|
if (ret != 0) {
|
|
free(response);
|
|
return CHUNK_ERR_NETWORK;
|
|
}
|
|
|
|
if (status_code == 416) {
|
|
free(response);
|
|
return CHUNK_ERR_RANGE_INVALID;
|
|
}
|
|
|
|
if (status_code != 200 && status_code != 206) {
|
|
free(response);
|
|
return CHUNK_ERR_SERVER_ERROR;
|
|
}
|
|
|
|
/* Parse chunk: [chunk_idx: 2B][payload: N B][crc32: 4B] */
|
|
if (actual_len < 6) { /* Minimum: 2 + 0 + 4 */
|
|
free(response);
|
|
return CHUNK_ERR_NETWORK;
|
|
}
|
|
|
|
uint16_t received_idx = (response[0] << 8) | response[1];
|
|
if (received_idx != chunk_idx) {
|
|
free(response);
|
|
return CHUNK_ERR_NETWORK; /* Chunk index mismatch */
|
|
}
|
|
|
|
/* Calculate payload length */
|
|
uint16_t total_chunk_size = client_state.config.chunk_size;
|
|
uint32_t remaining = client_state.file_size - (uint32_t)chunk_idx * total_chunk_size;
|
|
uint16_t expected_payload = (uint16_t)(remaining < total_chunk_size ? remaining : total_chunk_size);
|
|
|
|
if (actual_len != 2 + expected_payload + 4) {
|
|
/* Last chunk might have different size */
|
|
if (chunk_idx == client_state.total_chunks - 1) {
|
|
*payload_len = (uint16_t)(actual_len - 6);
|
|
} else {
|
|
free(response);
|
|
return CHUNK_ERR_NETWORK;
|
|
}
|
|
} else {
|
|
*payload_len = expected_payload;
|
|
}
|
|
|
|
/* Copy payload */
|
|
memcpy(buffer, response + 2, *payload_len);
|
|
|
|
/* Extract CRC32 */
|
|
*crc32 = (response[2 + *payload_len] << 24) |
|
|
(response[3 + *payload_len] << 16) |
|
|
(response[4 + *payload_len] << 8) |
|
|
response[5 + *payload_len];
|
|
|
|
free(response);
|
|
return CHUNK_ERR_OK;
|
|
}
|
|
|
|
/**
|
|
* Start file download
|
|
*/
|
|
chunk_err_t chunk_client_download(const char *filename) {
|
|
if (!filename) {
|
|
return CHUNK_ERR_INVALID_PARAM;
|
|
}
|
|
|
|
/* Store filename */
|
|
strncpy(client_state.current_file, filename, sizeof(client_state.current_file) - 1);
|
|
client_state.current_file[sizeof(client_state.current_file) - 1] = '\0';
|
|
|
|
/* Get file size */
|
|
chunk_err_t err = get_file_size(filename, &client_state.file_size);
|
|
if (err != CHUNK_ERR_OK) {
|
|
client_state.state = CHUNK_STATE_ERROR;
|
|
client_state.error_code = err;
|
|
if (client_state.config.on_error) {
|
|
client_state.config.on_error(err, client_state.config.user_ctx);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/* Calculate total chunks */
|
|
client_state.total_chunks = chunk_calc_total_chunks(
|
|
client_state.file_size, client_state.config.chunk_size);
|
|
|
|
client_state.current_chunk = 0;
|
|
client_state.state = CHUNK_STATE_TRANSFERRING;
|
|
client_state.cancel_requested = false;
|
|
|
|
/* Buffer for chunk payload */
|
|
uint8_t *payload = (uint8_t *)malloc(client_state.config.chunk_size);
|
|
if (!payload) {
|
|
client_state.state = CHUNK_STATE_ERROR;
|
|
client_state.error_code = CHUNK_ERR_MEMORY;
|
|
return CHUNK_ERR_MEMORY;
|
|
}
|
|
|
|
/* Download all chunks */
|
|
for (uint16_t i = 0; i < client_state.total_chunks; i++) {
|
|
if (client_state.cancel_requested) {
|
|
free(payload);
|
|
client_state.state = CHUNK_STATE_IDLE;
|
|
return CHUNK_ERR_OK; /* Cancel is not an error */
|
|
}
|
|
|
|
uint16_t payload_len;
|
|
uint32_t crc32;
|
|
|
|
err = download_chunk(i, payload, &payload_len, &crc32);
|
|
if (err != CHUNK_ERR_OK) {
|
|
free(payload);
|
|
client_state.state = CHUNK_STATE_ERROR;
|
|
client_state.error_code = err;
|
|
if (client_state.config.on_error) {
|
|
client_state.config.on_error(err, client_state.config.user_ctx);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/* Verify CRC32 */
|
|
uint32_t calculated_crc = chunk_crc32(payload, payload_len);
|
|
if (calculated_crc != crc32) {
|
|
free(payload);
|
|
client_state.state = CHUNK_STATE_ERROR;
|
|
client_state.error_code = CHUNK_ERR_CRC;
|
|
if (client_state.config.on_error) {
|
|
client_state.config.on_error(CHUNK_ERR_CRC, client_state.config.user_ctx);
|
|
}
|
|
return CHUNK_ERR_CRC;
|
|
}
|
|
|
|
/* Call chunk received callback */
|
|
if (client_state.config.on_chunk_received) {
|
|
client_state.config.on_chunk_received(i, payload, payload_len,
|
|
client_state.config.user_ctx);
|
|
}
|
|
|
|
/* Progress callback */
|
|
if (client_state.config.on_progress) {
|
|
client_state.config.on_progress(i + 1, client_state.total_chunks,
|
|
client_state.config.user_ctx);
|
|
}
|
|
|
|
client_state.current_chunk = i + 1;
|
|
}
|
|
|
|
free(payload);
|
|
|
|
/* Transfer complete */
|
|
client_state.state = CHUNK_STATE_COMPLETE;
|
|
if (client_state.config.on_complete) {
|
|
client_state.config.on_complete(client_state.file_size,
|
|
client_state.config.user_ctx);
|
|
}
|
|
|
|
return CHUNK_ERR_OK;
|
|
}
|
|
|
|
/**
|
|
* Resume download from specific chunk
|
|
*/
|
|
chunk_err_t chunk_client_resume(const char *filename, uint16_t start_chunk) {
|
|
if (!filename) {
|
|
return CHUNK_ERR_INVALID_PARAM;
|
|
}
|
|
|
|
/* Store filename */
|
|
strncpy(client_state.current_file, filename, sizeof(client_state.current_file) - 1);
|
|
client_state.current_file[sizeof(client_state.current_file) - 1] = '\0';
|
|
|
|
/* Get file size */
|
|
chunk_err_t err = get_file_size(filename, &client_state.file_size);
|
|
if (err != CHUNK_ERR_OK) {
|
|
client_state.state = CHUNK_STATE_ERROR;
|
|
client_state.error_code = err;
|
|
if (client_state.config.on_error) {
|
|
client_state.config.on_error(err, client_state.config.user_ctx);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/* Calculate total chunks */
|
|
client_state.total_chunks = chunk_calc_total_chunks(
|
|
client_state.file_size, client_state.config.chunk_size);
|
|
|
|
/* Validate start chunk */
|
|
if (start_chunk >= client_state.total_chunks) {
|
|
return CHUNK_ERR_RANGE_INVALID;
|
|
}
|
|
|
|
client_state.current_chunk = start_chunk;
|
|
client_state.state = CHUNK_STATE_RESUMING;
|
|
client_state.cancel_requested = false;
|
|
|
|
/* Buffer for chunk payload */
|
|
uint8_t *payload = (uint8_t *)malloc(client_state.config.chunk_size);
|
|
if (!payload) {
|
|
client_state.state = CHUNK_STATE_ERROR;
|
|
client_state.error_code = CHUNK_ERR_MEMORY;
|
|
return CHUNK_ERR_MEMORY;
|
|
}
|
|
|
|
/* Download remaining chunks */
|
|
for (uint16_t i = start_chunk; i < client_state.total_chunks; i++) {
|
|
if (client_state.cancel_requested) {
|
|
free(payload);
|
|
client_state.state = CHUNK_STATE_IDLE;
|
|
return CHUNK_ERR_OK;
|
|
}
|
|
|
|
uint16_t payload_len;
|
|
uint32_t crc32;
|
|
|
|
err = download_chunk(i, payload, &payload_len, &crc32);
|
|
if (err != CHUNK_ERR_OK) {
|
|
free(payload);
|
|
client_state.state = CHUNK_STATE_ERROR;
|
|
client_state.error_code = err;
|
|
if (client_state.config.on_error) {
|
|
client_state.config.on_error(err, client_state.config.user_ctx);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/* Verify CRC32 */
|
|
uint32_t calculated_crc = chunk_crc32(payload, payload_len);
|
|
if (calculated_crc != crc32) {
|
|
free(payload);
|
|
client_state.state = CHUNK_STATE_ERROR;
|
|
client_state.error_code = CHUNK_ERR_CRC;
|
|
if (client_state.config.on_error) {
|
|
client_state.config.on_error(CHUNK_ERR_CRC, client_state.config.user_ctx);
|
|
}
|
|
return CHUNK_ERR_CRC;
|
|
}
|
|
|
|
/* Call chunk received callback */
|
|
if (client_state.config.on_chunk_received) {
|
|
client_state.config.on_chunk_received(i, payload, payload_len,
|
|
client_state.config.user_ctx);
|
|
}
|
|
|
|
/* Progress callback */
|
|
if (client_state.config.on_progress) {
|
|
client_state.config.on_progress(i + 1, client_state.total_chunks,
|
|
client_state.config.user_ctx);
|
|
}
|
|
|
|
client_state.current_chunk = i + 1;
|
|
}
|
|
|
|
free(payload);
|
|
|
|
/* Transfer complete */
|
|
client_state.state = CHUNK_STATE_COMPLETE;
|
|
if (client_state.config.on_complete) {
|
|
client_state.config.on_complete(client_state.file_size,
|
|
client_state.config.user_ctx);
|
|
}
|
|
|
|
return CHUNK_ERR_OK;
|
|
}
|
|
|
|
/**
|
|
* Cancel current transfer
|
|
*/
|
|
void chunk_client_cancel(void) {
|
|
client_state.cancel_requested = true;
|
|
}
|
|
|
|
/**
|
|
* Get current transfer state
|
|
*/
|
|
chunk_state_t chunk_client_get_state(void) {
|
|
return client_state.state;
|
|
}
|
|
|
|
/**
|
|
* Get current progress
|
|
*/
|
|
chunk_err_t chunk_client_get_progress(uint16_t *current_chunk, uint16_t *total_chunks) {
|
|
if (!current_chunk || !total_chunks) {
|
|
return CHUNK_ERR_INVALID_PARAM;
|
|
}
|
|
|
|
*current_chunk = client_state.current_chunk;
|
|
*total_chunks = client_state.total_chunks;
|
|
|
|
return CHUNK_ERR_OK;
|
|
}
|
|
|
|
/**
|
|
* Cleanup client resources
|
|
*/
|
|
void chunk_client_cleanup(void) {
|
|
memset(&client_state, 0, sizeof(client_state));
|
|
}
|