blob: a32ead7abb9f1f86acad66d4b5220c3b0584dd49 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <tftp/tftp.h>
#include <unittest/unittest.h>
#include <limits.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <atomic>
// This test simulates a tftp file transfer by running two threads. Both the
// file and transport interfaces are implemented in memory buffers.
typedef enum { DIR_SEND, DIR_RECEIVE } xfer_dir_t;
struct test_params {
xfer_dir_t direction;
uint32_t filesz;
uint16_t winsz;
uint16_t blksz;
};
static uint8_t *src_file;
static uint8_t *dst_file;
/* FAUX FILES INTERFACE */
typedef struct {
uint8_t* buf;
char filename[PATH_MAX + 1];
size_t filesz;
} file_info_t;
// Allocate our src and dst buffers, filling both with random values.
int initialize_files(struct test_params* tp) {
src_file = reinterpret_cast<uint8_t*>(malloc(tp->filesz));
dst_file = reinterpret_cast<uint8_t*>(malloc(tp->filesz));
if (!src_file || !dst_file) {
return 1;
}
int* src_as_ints = (int*)src_file;
int* dst_as_ints = (int*)dst_file;
size_t ndx;
srand(0);
for (ndx = 0; ndx < tp->filesz / sizeof(int); ndx++) {
src_as_ints[ndx] = rand();
dst_as_ints[ndx] = rand();
}
for (ndx = (tp->filesz / sizeof(int)) * sizeof(int);
ndx < tp->filesz;
ndx++) {
src_file[ndx] = static_cast<uint8_t>(rand());
dst_file[ndx] = static_cast<uint8_t>(rand());
}
return 0;
}
int compare_files(size_t filesz) {
return memcmp(src_file, dst_file, filesz);
}
const char* file_get_filename(file_info_t* file_info) {
return file_info->filename;
}
ssize_t file_open_read(const char* filename, void* file_cookie) {
auto* file_info = reinterpret_cast<file_info_t*>(file_cookie);
file_info->buf = src_file;
strncpy(file_info->filename, filename, PATH_MAX);
file_info->filename[PATH_MAX] = '\0';
return file_info->filesz;
}
tftp_status file_open_write(const char* filename,
size_t size,
void* file_cookie) {
auto* file_info = reinterpret_cast<file_info_t*>(file_cookie);
file_info->buf = dst_file;
file_info->filename[PATH_MAX] = '\0';
strncpy(file_info->filename, filename, PATH_MAX);
return TFTP_NO_ERROR;
}
// Every SHORT_READ_FREQ writes we will read a smaller amount instead, to verify behavior when
// a read operation returns a length less than the requested amount.
#define SHORT_READ_FREQ 10
tftp_status file_read(void* data, size_t* length, off_t offset,
void* file_cookie) {
auto* file_info = reinterpret_cast<file_info_t*>(file_cookie);
if ((size_t)offset > file_info->filesz) {
// Something has gone wrong in libtftp
return TFTP_ERR_INTERNAL;
}
if ((offset + *length) > file_info->filesz) {
*length = file_info->filesz - offset;
}
static size_t read_count = 0;
if (read_count++ % SHORT_READ_FREQ == 0) {
*length /= 2;
}
memcpy(data, &file_info->buf[offset], *length);
return TFTP_NO_ERROR;
}
// Every SHORT_WRITE_FREQ writes we will write a smaller amount instead, to verify behavior when
// a write operation returns a length less than the requested amount.
#define SHORT_WRITE_FREQ 10
tftp_status file_write(const void* data, size_t* length, off_t offset,
void* file_cookie) {
auto* file_info = reinterpret_cast<file_info_t*>(file_cookie);
if (((size_t)offset > file_info->filesz) || ((offset + *length) > file_info->filesz)) {
// Something has gone wrong in libtftp
return TFTP_ERR_INTERNAL;
}
static size_t write_count = 0;
if (write_count++ % SHORT_WRITE_FREQ == 0) {
*length /= 2;
}
memcpy(&file_info->buf[offset], data, *length);
return TFTP_NO_ERROR;
}
void file_close(void* file_cookie) {
}
/* FAUX SOCKET INTERFACE */
#define FAKE_SOCK_BUF_SZ 65536
typedef struct {
uint8_t buf[FAKE_SOCK_BUF_SZ];
size_t size = FAKE_SOCK_BUF_SZ;
std::atomic<size_t> read_ndx;
std::atomic<size_t> write_ndx;
} fake_socket_t;
static fake_socket_t client_out_socket;
static fake_socket_t server_out_socket;
typedef struct {
fake_socket_t* in_sock;
fake_socket_t* out_sock;
} transport_info_t;
void clear_sockets(void) {
client_out_socket.read_ndx.store(0);
client_out_socket.write_ndx.store(0);
server_out_socket.read_ndx.store(0);
server_out_socket.write_ndx.store(0);
}
// Initialize "sockets" for either client or server.
void transport_init(transport_info_t* transport_info, bool is_server) {
if (is_server) {
transport_info->in_sock = &client_out_socket;
transport_info->out_sock = &server_out_socket;
} else {
transport_info->in_sock = &server_out_socket;
transport_info->out_sock = &client_out_socket;
}
}
// Write to our circular message buffer.
void write_to_buf(fake_socket_t* sock, void* data, size_t size) {
uint8_t* in_buf = reinterpret_cast<uint8_t*>(data);
uint8_t* out_buf = sock->buf;
size_t curr_offset = sock->write_ndx.load() % sock->size;
if (curr_offset + size <= sock->size) {
memcpy(&out_buf[curr_offset], in_buf, size);
} else {
size_t first_size = sock->size - curr_offset;
size_t second_size = size - first_size;
memcpy(out_buf + curr_offset, in_buf, first_size);
memcpy(out_buf, in_buf + first_size, second_size);
}
sock->write_ndx.fetch_add(size);
}
// Send a message. Note that the buffer's read_ndx and write_ndx don't wrap,
// which makes it easier to recognize underflow.
tftp_status transport_send(void* data, size_t len, void* transport_cookie) {
auto* transport_info = reinterpret_cast<transport_info_t*>(transport_cookie);
fake_socket_t* sock = transport_info->out_sock;
while ((sock->write_ndx.load() + sizeof(len) + len - sock->read_ndx.load())
> sock->size) {
// Wait for the other thread to catch up
usleep(10);
}
write_to_buf(sock, &len, sizeof(len));
write_to_buf(sock, data, len);
return TFTP_NO_ERROR;
}
// Read from our circular message buffer. If |move_ptr| is false, just peeks at
// the data (reads without updating the read pointer).
void read_from_buf(fake_socket_t* sock, void* data, size_t size,
bool move_ptr) {
uint8_t* in_buf = sock->buf;
uint8_t* out_buf = reinterpret_cast<uint8_t*>(data);
size_t curr_offset = sock->read_ndx.load() % sock->size;
if (curr_offset + size <= sock->size) {
memcpy(out_buf, &in_buf[curr_offset], size);
} else {
size_t first_size = sock->size - curr_offset;
size_t second_size = size - first_size;
memcpy(out_buf, in_buf + curr_offset, first_size);
memcpy(out_buf + first_size, in_buf, second_size);
}
if (move_ptr) {
sock->read_ndx.fetch_add(size);
}
}
// Receive a message. Note that the buffer's read_ndx and write_ndx don't
// wrap, which makes it easier to recognize underflow.
int transport_recv(void* data, size_t len, bool block, void* transport_cookie) {
auto* transport_info = reinterpret_cast<transport_info_t*>(transport_cookie);
if (block) {
while ((transport_info->in_sock->read_ndx.load() + sizeof(size_t)) >=
transport_info->in_sock->write_ndx.load()) {
usleep(10);
}
} else if ((transport_info->in_sock->read_ndx.load() + sizeof(size_t)) >=
transport_info->in_sock->write_ndx.load()) {
return TFTP_ERR_TIMED_OUT;
}
size_t block_len;
read_from_buf(transport_info->in_sock, &block_len, sizeof(block_len),
false);
if (block_len > len) {
return TFTP_ERR_BUFFER_TOO_SMALL;
}
transport_info->in_sock->read_ndx.fetch_add(sizeof(block_len));
read_from_buf(transport_info->in_sock, data, block_len, true);
return static_cast<int>(block_len);
}
int transport_timeout_set(uint32_t timeout_ms, void* transport_cookie) {
return 0;
}
/// SEND THREAD
bool run_client_test(struct test_params* tp) {
BEGIN_HELPER;
// Configure TFTP session
tftp_session* session;
size_t session_size = tftp_sizeof_session();
void* session_buf = malloc(session_size);
ASSERT_NONNULL(session_buf, "memory allocation failed");
tftp_status status = tftp_init(&session, session_buf, session_size);
ASSERT_EQ(status, TFTP_NO_ERROR, "unable to initialize a tftp session");
// Configure file interface
file_info_t file_info;
file_info.filesz = tp->filesz;
tftp_file_interface file_callbacks = { file_open_read,
file_open_write,
file_read,
file_write,
file_close };
status = tftp_session_set_file_interface(session, &file_callbacks);
ASSERT_EQ(status, TFTP_NO_ERROR, "could not set file interface");
// Configure transport interface
transport_info_t transport_info;
transport_init(&transport_info, false);
tftp_transport_interface transport_callbacks = { transport_send,
transport_recv,
transport_timeout_set };
status = tftp_session_set_transport_interface(session,
&transport_callbacks);
ASSERT_EQ(status, TFTP_NO_ERROR, "could not set transport interface");
// Allocate intermediate buffers
size_t buf_sz = tp->blksz > PATH_MAX ?
tp->blksz + 2 : PATH_MAX + 2;
char* msg_in_buf = reinterpret_cast<char*>(malloc(buf_sz));
ASSERT_NONNULL(msg_in_buf, "memory allocation failure");
char* msg_out_buf = reinterpret_cast<char*>(malloc(buf_sz));
ASSERT_NONNULL(msg_out_buf, "memory allocation failure");
char err_msg_buf[128];
// Set our preferred transport options
tftp_set_options(session, &tp->blksz, NULL, &tp->winsz);
tftp_request_opts opts = {};
opts.inbuf = msg_in_buf;
opts.inbuf_sz = buf_sz;
opts.outbuf = msg_out_buf;
opts.outbuf_sz = buf_sz;
opts.err_msg = err_msg_buf;
opts.err_msg_sz = sizeof(err_msg_buf);
if (tp->direction == DIR_SEND) {
status = tftp_push_file(session, &transport_info, &file_info, "abc.txt",
"xyz.txt", &opts);
EXPECT_GE(status, 0, "failed to send file");
} else {
status = tftp_pull_file(session, &transport_info, &file_info, "abc.txt",
"xyz.txt", &opts);
EXPECT_GE(status, 0, "failed to receive file");
}
free(session);
END_HELPER;
}
void* tftp_client_main(void* arg) {
auto* tp = reinterpret_cast<test_params*>(arg);
run_client_test(tp);
pthread_exit(NULL);
}
/// RECV THREAD
bool run_server_test(struct test_params* tp) {
BEGIN_HELPER;
// Configure TFTP session
tftp_session* session;
size_t session_size = tftp_sizeof_session();
void* session_buf = malloc(session_size);
ASSERT_NONNULL(session_buf, "memory allocation failed");
tftp_status status = tftp_init(&session, session_buf, session_size);
ASSERT_EQ(status, TFTP_NO_ERROR, "unable to initiate a tftp session");
// Configure file interface
file_info_t file_info;
file_info.filesz = tp->filesz;
tftp_file_interface file_callbacks = { file_open_read,
file_open_write,
file_read,
file_write,
file_close };
status = tftp_session_set_file_interface(session, &file_callbacks);
ASSERT_EQ(status, TFTP_NO_ERROR, "could not set file interface");
// Configure transport interface
transport_info_t transport_info;
transport_init(&transport_info, true);
tftp_transport_interface transport_callbacks = { transport_send,
transport_recv,
transport_timeout_set };
status = tftp_session_set_transport_interface(session,
&transport_callbacks);
ASSERT_EQ(status, TFTP_NO_ERROR, "could not set transport interface");
// Allocate intermediate buffers
size_t buf_sz = tp->blksz > PATH_MAX ?
tp->blksz + 2 : PATH_MAX + 2;
char* msg_in_buf = reinterpret_cast<char*>(malloc(buf_sz));
ASSERT_NONNULL(msg_in_buf, "memory allocation failure");
char* msg_out_buf = reinterpret_cast<char*>(malloc(buf_sz));
ASSERT_NONNULL(msg_out_buf, "memory allocation failure");
char err_msg_buf[128];
tftp_handler_opts opts = { .inbuf = msg_in_buf,
.inbuf_sz = buf_sz,
.outbuf = msg_out_buf,
.outbuf_sz = &buf_sz,
.err_msg = err_msg_buf,
.err_msg_sz = sizeof(err_msg_buf) };
do {
status = tftp_service_request(session, &transport_info, &file_info,
&opts);
} while (status == TFTP_NO_ERROR);
EXPECT_EQ(status, TFTP_TRANSFER_COMPLETED, "failed to receive file");
free(session);
END_HELPER;
}
void* tftp_server_main(void* arg) {
auto* tp = reinterpret_cast<test_params*>(arg);
run_server_test(tp);
pthread_exit(NULL);
}
bool run_one_test(struct test_params* tp) {
BEGIN_TEST;
int init_result = initialize_files(tp);
ASSERT_EQ(init_result, 0, "failure to initialize state");
clear_sockets();
pthread_t client_thread, server_thread;
pthread_create(&client_thread, NULL, tftp_client_main, tp);
pthread_create(&server_thread, NULL, tftp_server_main, tp);
pthread_join(client_thread, NULL);
pthread_join(server_thread, NULL);
int compare_result = compare_files(tp->filesz);
EXPECT_EQ(compare_result, 0, "output file mismatch");
END_TEST;
}
bool test_tftp_send_file(void) {
struct test_params tp = {.direction = DIR_SEND, .filesz = 1000000, .winsz = 20, .blksz = 1000};
return run_one_test(&tp);
}
bool test_tftp_send_file_wrapping_block_count(void) {
// Wraps block count 4 times
struct test_params tp = {.direction = DIR_SEND, .filesz = 2100000, .winsz = 9999, .blksz = 8};
return run_one_test(&tp);
}
bool test_tftp_send_file_lg_window(void) {
// Make sure that a window size > 255 works properly
struct test_params tp = {.direction = DIR_SEND, .filesz = 1000000, .winsz = 1024,
.blksz = 1024};
return run_one_test(&tp);
}
bool test_tftp_receive_file(void) {
struct test_params tp = {.direction = DIR_RECEIVE, .filesz = 1000000, .winsz = 20,
.blksz = 1000};
return run_one_test(&tp);
}
bool test_tftp_receive_file_wrapping_block_count(void) {
// Wraps block count 4 times
struct test_params tp = {.direction = DIR_RECEIVE, .filesz = 2100000, .winsz = 8192,
.blksz = 8};
return run_one_test(&tp);
}
bool test_tftp_receive_file_lg_window(void) {
// Make sure that a window size > 255 works properly
struct test_params tp = {.direction = DIR_RECEIVE, .filesz = 1000000, .winsz = 1024,
.blksz = 1024};
return run_one_test(&tp);
}
BEGIN_TEST_CASE(tftp_transfer_file)
RUN_TEST(test_tftp_send_file)
RUN_TEST(test_tftp_send_file_wrapping_block_count)
RUN_TEST(test_tftp_send_file_lg_window)
RUN_TEST(test_tftp_receive_file)
RUN_TEST(test_tftp_receive_file_wrapping_block_count)
RUN_TEST(test_tftp_receive_file_lg_window)
END_TEST_CASE(tftp_transfer_file)