Initial commit: OTA chunk transfer protocol implementation

- 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)
This commit is contained in:
km
2026-03-30 06:35:25 +09:00
commit b1c3ec3af4
10 changed files with 1522 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
/**
* @file test_crc32.c
* @brief CRC32 Test Suite
*/
#include "protocol.h"
#include <stdio.h>
#include <string.h>
static int passed = 0;
static int failed = 0;
#define TEST_ASSERT(cond, msg) do { \
if (cond) { \
printf("[PASS] %s\n", msg); \
passed++; \
} else { \
printf("[FAIL] %s\n", msg); \
failed++; \
} \
} while(0)
/* Test vector from IEEE 802.3 */
static void test_crc32_basic(void) {
printf("\n--- CRC32 Basic Tests ---\n");
/* Test 1: Empty data */
uint32_t crc = chunk_crc32(NULL, 0);
TEST_ASSERT(crc == 0x00000000UL, "Empty data CRC = 0x00000000");
/* Test 2: Single byte */
uint8_t data1[] = {0x41}; /* 'A' */
crc = chunk_crc32(data1, 1);
TEST_ASSERT(crc == 0xD3D99E8BUL, "Single byte 'A' CRC = 0xD3D99E8B");
/* Test 3: "foobar" */
uint8_t data2[] = "foobar";
crc = chunk_crc32(data2, 6);
TEST_ASSERT(crc == 0x9EF61F95UL, "'foobar' CRC = 0x9EF61F95");
/* Test 4: Incremental update */
uint32_t crc1 = chunk_crc32((uint8_t*)"foo", 3);
uint32_t crc2 = chunk_crc32_update(crc1, (uint8_t*)"bar", 3);
uint32_t crc3 = chunk_crc32(data2, 6);
TEST_ASSERT(crc2 == crc3, "Incremental update matches single call");
}
/* Test chunk size calculations */
static void test_chunk_calculations(void) {
printf("\n--- Chunk Calculation Tests ---\n");
/* Test 1: Exact multiple */
uint16_t chunks = chunk_calc_total_chunks(4096, 1024);
TEST_ASSERT(chunks == 4, "4096/1024 = 4 chunks");
/* Test 2: Not exact multiple */
chunks = chunk_calc_total_chunks(4097, 1024);
TEST_ASSERT(chunks == 5, "4097/1024 = 5 chunks");
/* Test 3: Last chunk payload size */
uint16_t payload = chunk_calc_payload_size(4097, 4, 1024);
TEST_ASSERT(payload == 1, "Last chunk of 4097 bytes = 1 byte");
/* Test 4: Middle chunk payload size */
payload = chunk_calc_payload_size(4097, 2, 1024);
TEST_ASSERT(payload == 1024, "Middle chunk = 1024 bytes");
}
/* Test CRC32 table initialization */
static void test_crc32_table(void) {
printf("\n--- CRC32 Table Tests ---\n");
/* Call multiple times to verify idempotent initialization */
chunk_crc32((uint8_t*)"test", 4);
chunk_crc32((uint8_t*)"test", 4);
chunk_crc32((uint8_t*)"test", 4);
TEST_ASSERT(1, "Multiple initializations OK");
}
int main(void) {
printf("========================================\n");
printf(" CRC32 Test Suite\n");
printf("========================================\n");
test_crc32_basic();
test_chunk_calculations();
test_crc32_table();
printf("\n========================================\n");
printf(" Results: %d passed, %d failed\n", passed, failed);
printf("========================================\n");
return failed == 0 ? 0 : 1;
}
+215
View File
@@ -0,0 +1,215 @@
/**
* @file test_server.js
* @brief Server Test Suite
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const ChunkServer = require('../server/server');
const PORT = 18080;
const TEST_DIR = path.join(__dirname, 'test_files');
let passed = 0;
let failed = 0;
function assert(cond, msg) {
if (cond) {
console.log(`[PASS] ${msg}`);
passed++;
} else {
console.log(`[FAIL] ${msg}`);
failed++;
}
}
/**
* HTTP GET helper
*/
function httpRequest(options, expectedStatus = 200) {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let data = Buffer.alloc(0);
res.on('data', chunk => {
data = Buffer.concat([data, chunk]);
});
res.on('end', () => {
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: data
});
});
});
req.on('error', reject);
req.end();
});
}
/**
* Setup test files
*/
function setupTestFiles() {
if (!fs.existsSync(TEST_DIR)) {
fs.mkdirSync(TEST_DIR);
}
// Create test file: 3072 bytes (3 chunks of 1024)
const testData = Buffer.alloc(3072);
for (let i = 0; i < 3072; i++) {
testData[i] = i % 256;
}
fs.writeFileSync(path.join(TEST_DIR, 'test.bin'), testData);
// Create small file: 100 bytes
const smallData = Buffer.alloc(100);
for (let i = 0; i < 100; i++) {
smallData[i] = 0x42;
}
fs.writeFileSync(path.join(TEST_DIR, 'small.bin'), smallData);
}
/**
* Cleanup test files
*/
function cleanupTestFiles() {
try {
fs.rmSync(TEST_DIR, { recursive: true, force: true });
} catch (e) {
// Ignore
}
}
/**
* Test suite
*/
async function runTests() {
console.log("========================================");
console.log(" Chunk Server Test Suite");
console.log("========================================\n");
// Setup
setupTestFiles();
const server = new ChunkServer({
port: PORT,
rootPath: TEST_DIR,
chunkSize: 1024
});
await server.start();
console.log("\n--- Server Tests ---");
try {
// Test 1: Get first chunk
const res1 = await httpRequest({
hostname: 'localhost',
port: PORT,
path: '/ota?file=test.bin',
method: 'GET'
});
assert(res1.statusCode === 200, "First chunk returns 200");
assert(res1.headers['content-type'] === 'application/octet-stream', "Content-Type is octet-stream");
assert(res1.headers['x-file-size'] === '3072', "X-File-Size header present");
assert(res1.headers['x-total-chunks'] === '3', "X-Total-Chunks header present");
assert(res1.body.length === 2 + 1024 + 4, "First chunk size correct (header + payload + crc)");
// Verify chunk index in payload
assert(res1.body.readUInt16LE(0) === 0, "Chunk index is 0");
// Test 2: Get second chunk
const res2 = await httpRequest({
hostname: 'localhost',
port: PORT,
path: '/ota?file=test.bin&chunk=1',
method: 'GET'
});
assert(res2.statusCode === 206, "Second chunk returns 206");
assert(res2.body.readUInt16LE(0) === 1, "Chunk index is 1");
assert(res2.body.length === 2 + 1024 + 4, "Second chunk size correct");
// Test 3: Get last chunk (smaller)
const res3 = await httpRequest({
hostname: 'localhost',
port: PORT,
path: '/ota?file=test.bin&chunk=2',
method: 'GET'
});
assert(res3.statusCode === 206, "Last chunk returns 206");
assert(res3.body.readUInt16LE(0) === 2, "Chunk index is 2");
assert(res3.body.length === 2 + 1024 + 4, "Last chunk size correct");
// Test 4: Invalid chunk index
const res4 = await httpRequest({
hostname: 'localhost',
port: PORT,
path: '/ota?file=test.bin&chunk=10',
method: 'GET'
});
assert(res4.statusCode === 416, "Invalid chunk returns 416");
// Test 5: File not found
const res5 = await httpRequest({
hostname: 'localhost',
port: PORT,
path: '/ota?file=nonexistent.bin',
method: 'GET'
});
assert(res5.statusCode === 404, "Missing file returns 404");
// Test 6: Missing file parameter
const res6 = await httpRequest({
hostname: 'localhost',
port: PORT,
path: '/ota',
method: 'GET'
});
assert(res6.statusCode === 400, "Missing file param returns 400");
// Test 7: Small file (single chunk)
const res7 = await httpRequest({
hostname: 'localhost',
port: PORT,
path: '/ota?file=small.bin',
method: 'GET'
});
assert(res7.statusCode === 200, "Small file returns 200");
assert(res7.headers['x-total-chunks'] === '1', "Small file has 1 chunk");
assert(res7.body.length === 2 + 100 + 4, "Small file chunk size correct");
// Test 8: Verify CRC32
const payload = res1.body.slice(2, 2 + 1024);
const receivedCrc = res1.body.readUInt32LE(2 + 1024);
const calculatedCrc = ChunkServer.crc32(payload);
console.log(`Received CRC: ${receivedCrc.toString(16)}`);
console.log(`Calculated CRC: ${calculatedCrc.toString(16)}`);
console.log(`Calculated >>>0: ${(calculatedCrc >>> 0).toString(16)}`);
assert(receivedCrc === (calculatedCrc >>> 0), "CRC32 verification passed");
} catch (err) {
console.error("Test error:", err);
failed++;
} finally {
await server.stop();
cleanupTestFiles();
}
console.log("\n========================================");
console.log(` Results: ${passed} passed, ${failed} failed`);
console.log("========================================");
process.exit(failed === 0 ? 0 : 1);
}
runTests().catch(err => {
console.error("Fatal error:", err);
process.exit(1);
});