| // 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) |