193 changes: 193 additions & 0 deletions libc/src/__support/File/file.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//===--- A platform independent file data structure -------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_LIBC_SRC_SUPPORT_OSUTIL_FILE_H
#define LLVM_LIBC_SRC_SUPPORT_OSUTIL_FILE_H

#include <stddef.h>
#include <stdint.h>

namespace __llvm_libc {

// This a generic base class to encapsulate a platform independent file data
// structure. Platform specific specializations should create a subclass as
// suitable for their platform.
class File {
public:
using LockFunc = void(File *);
using UnlockFunc = void(File *);

using WriteFunc = size_t(File *, const void *, size_t);
using ReadFunc = size_t(File *, void *, size_t);
using SeekFunc = int(File *, long, int);
using CloseFunc = int(File *);
using FlushFunc = int(File *);

using ModeFlags = uint32_t;

// The three different types of flags below are to be used with '|' operator.
// Their values correspond to mutually exclusive bits in a 32-bit unsigned
// integer value. A flag set can include both READ and WRITE if the file
// is opened in update mode (ie. if the file was opened with a '+' the mode
// string.)
enum class OpenMode : ModeFlags {
READ = 0x1,
WRITE = 0x2,
APPEND = 0x4,
};

// Denotes a file opened in binary mode (which is specified by including
// the 'b' character in teh mode string.)
enum class ContentType : ModeFlags {
BINARY = 0x10,
};

// Denotes a file to be created for writing.
enum class CreateType : ModeFlags {
EXCLUSIVE = 0x100,
};

private:
enum class FileOp : uint8_t { NONE, READ, WRITE, SEEK };

// Platfrom specific functions which create new file objects should initialize
// these fields suitably via the constructor. Typically, they should be simple
// syscall wrappers for the corresponding functionality.
WriteFunc *platform_write;
ReadFunc *platform_read;
SeekFunc *platform_seek;
CloseFunc *platform_close;
FlushFunc *platform_flush;

// Platform specific functions to lock and unlock file for mutually exclusive
// access from threads in a multi-threaded application.
LockFunc *platform_lock;
UnlockFunc *platform_unlock;

void *buf; // Pointer to the stream buffer for buffered streams
size_t bufsize; // Size of the buffer pointed to by |buf|.

// Buffering mode to used to buffer.
int bufmode;

// If own_buf is true, the |buf| is owned by the stream and will be
// free-ed when close method is called on the stream.
bool own_buf;

// The mode in which the file was opened.
ModeFlags mode;

// Current read or write pointer.
size_t pos;

// Represents the previous operation that was performed.
FileOp prev_op;

// When the buffer is used as a read buffer, read_limit is the upper limit
// of the index to which the buffer can be read until.
size_t read_limit;

bool eof;
bool err;

protected:
bool write_allowed() const {
return mode & (static_cast<ModeFlags>(OpenMode::WRITE) |
static_cast<ModeFlags>(OpenMode::APPEND));
}

bool read_allowed() const {
return mode & static_cast<ModeFlags>(OpenMode::READ);
}

public:
// We want this constructor to be constexpr so that global file objects
// like stdout do not require invocation of the constructor which can
// potentially lead to static initialization order fiasco.
constexpr File(WriteFunc *wf, ReadFunc *rf, SeekFunc *sf, CloseFunc *cf,
FlushFunc *ff, LockFunc *lf, UnlockFunc *ulf, void *buffer,
size_t buffer_size, int buffer_mode, bool owned,
ModeFlags modeflags)
: platform_write(wf), platform_read(rf), platform_seek(sf),
platform_close(cf), platform_flush(ff), platform_lock(lf),
platform_unlock(ulf), buf(buffer), bufsize(buffer_size),
bufmode(buffer_mode), own_buf(owned), mode(modeflags), pos(0),
prev_op(FileOp::NONE), read_limit(0), eof(false), err(false) {}

// This function helps initialize the various fields of the File data
// structure after a allocating memory for it via a call to malloc.
static void init(File *f, WriteFunc *wf, ReadFunc *rf, SeekFunc *sf,
CloseFunc *cf, FlushFunc *ff, LockFunc *lf, UnlockFunc *ulf,
void *buffer, size_t buffer_size, int buffer_mode,
bool owned, ModeFlags modeflags) {
f->platform_write = wf;
f->platform_read = rf;
f->platform_seek = sf;
f->platform_close = cf;
f->platform_flush = ff;
f->platform_lock = lf;
f->platform_unlock = ulf;
f->buf = reinterpret_cast<uint8_t *>(buffer);
f->bufsize = buffer_size;
f->bufmode = buffer_mode;
f->own_buf = owned;
f->mode = modeflags;

f->prev_op = FileOp::NONE;
f->read_limit = f->pos = 0;
f->eof = f->err = false;
}

// Buffered write of |len| bytes from |data|.
size_t write(const void *data, size_t len);

// Buffered read of |len| bytes into |data|.
size_t read(void *data, size_t len);

int seek(long offset, int whence);

// If buffer has data written to it, flush it out. Does nothing if the
// buffer is currently being used as a read buffer.
int flush();

// Sets the internal buffer to |buffer| with buffering mode |mode|.
// |size| is the size of |buffer|. This new |buffer| is owned by the
// stream only if |owned| is true.
void set_buffer(void *buffer, size_t size, bool owned);

// Closes the file stream and frees up all resources owned by it.
int close();

void lock() { platform_lock(this); }
void unlock() { platform_unlock(this); }

bool error() const { return err; }
void clearerr() { err = false; }
bool iseof() const { return eof; }

// Returns an bit map of flags corresponding to enumerations of
// OpenMode, ContentType and CreateType.
static ModeFlags mode_flags(const char *mode);
};

// This is a convenience RAII class to lock and unlock file objects.
class FileLock {
File *file;

public:
explicit FileLock(File *f) : file(f) { file->lock(); }

~FileLock() { file->unlock(); }

FileLock(const FileLock &) = delete;
FileLock(FileLock &&) = delete;
};

} // namespace __llvm_libc

#endif // LLVM_LIBC_SRC_SUPPORT_OSUTIL_FILE_H
1 change: 1 addition & 0 deletions libc/test/src/__support/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ add_custom_command(TARGET libc_str_to_float_comparison_test
VERBATIM)

add_subdirectory(CPP)
add_subdirectory(File)
add_subdirectory(OSUtil)
16 changes: 16 additions & 0 deletions libc/test/src/__support/File/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

add_libc_unittest(
file_test
SUITE
libc_support_unittests
SRCS
file_test.cpp
DEPENDS
libc.include.stdio
libc.include.stdlib
libc.src.__support.File.file
)

target_link_libraries(
libc.test.src.__support.File.file_test PRIVATE LibcMemoryHelpers
)
321 changes: 321 additions & 0 deletions libc/test/src/__support/File/file_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
//===-- Unittests for platform independent file class ---------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "src/__support/File/file.h"
#include "utils/UnitTest/MemoryMatcher.h"
#include "utils/UnitTest/Test.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

using ModeFlags = __llvm_libc::File::ModeFlags;
using MemoryView = __llvm_libc::memory::testing::MemoryView;

class StringFile : public __llvm_libc::File {
static constexpr size_t SIZE = 512;
size_t pos;
char str[SIZE] = {0};
size_t eof_marker;
bool write_append;

static size_t str_read(__llvm_libc::File *f, void *data, size_t len);
static size_t str_write(__llvm_libc::File *f, const void *data, size_t len);
static int str_seek(__llvm_libc::File *f, long offset, int whence);
static int str_close(__llvm_libc::File *f) { return 0; }
static int str_flush(__llvm_libc::File *f) { return 0; }

// TODO: Add a proper locking system and tests which exercise that.
static void str_lock(__llvm_libc::File *f) {}
static void str_unlock(__llvm_libc::File *f) {}

public:
explicit StringFile(char *buffer, size_t buflen, int bufmode, bool owned,
ModeFlags modeflags)
: __llvm_libc::File(&str_write, &str_read, &str_seek, &str_close,
&str_flush, &str_lock, &str_unlock, buffer, buflen,
bufmode, owned, modeflags),
pos(0), eof_marker(0), write_append(false) {
if (modeflags & static_cast<ModeFlags>(__llvm_libc::File::OpenMode::APPEND))
write_append = true;
}

void init(char *buffer, size_t buflen, int bufmode, bool owned,
ModeFlags modeflags) {
File::init(this, &str_write, &str_read, &str_seek, &str_close, &str_flush,
&str_lock, &str_unlock, buffer, buflen, bufmode, owned,
modeflags);
pos = eof_marker = 0;
if (modeflags & static_cast<ModeFlags>(__llvm_libc::File::OpenMode::APPEND))
write_append = true;
else
write_append = false;
}

void reset() { pos = 0; }
size_t get_pos() const { return pos; }
char *get_str() { return str; }

// Use this method to prefill the file.
void reset_and_fill(const char *data, size_t len) {
size_t i;
for (i = 0; i < len && i < SIZE; ++i) {
str[i] = data[i];
}
pos = 0;
eof_marker = i;
}
};

size_t StringFile::str_read(__llvm_libc::File *f, void *data, size_t len) {
StringFile *sf = static_cast<StringFile *>(f);
if (sf->pos >= sf->eof_marker)
return 0;
size_t i = 0;
for (i = 0; i < len; ++i)
reinterpret_cast<char *>(data)[i] = sf->str[sf->pos + i];
sf->pos += i;
return i;
}

size_t StringFile::str_write(__llvm_libc::File *f, const void *data,
size_t len) {
StringFile *sf = static_cast<StringFile *>(f);
if (sf->write_append)
sf->pos = sf->eof_marker;
if (sf->pos >= SIZE)
return 0;
size_t i = 0;
for (i = 0; i < len && sf->pos < SIZE; ++i, ++sf->pos)
sf->str[sf->pos] = reinterpret_cast<const char *>(data)[i];
// Move the eof marker if the data was written beyond the current eof marker.
if (sf->pos > sf->eof_marker)
sf->eof_marker = sf->pos;
return i;
}

int StringFile::str_seek(__llvm_libc::File *f, long offset, int whence) {
StringFile *sf = static_cast<StringFile *>(f);
if (whence == SEEK_SET)
sf->pos = offset;
if (whence == SEEK_CUR)
sf->pos += offset;
if (whence == SEEK_END)
sf->pos = SIZE + offset;
return 0;
}

StringFile *new_string_file(char *buffer, size_t buflen, int bufmode,
bool owned, const char *mode) {
StringFile *f = reinterpret_cast<StringFile *>(malloc(sizeof(StringFile)));
f->init(buffer, buflen, bufmode, owned, __llvm_libc::File::mode_flags(mode));
return f;
}

TEST(LlvmLibcFileTest, WriteOnly) {
const char data[] = "hello, file";
constexpr size_t FILE_BUFFER_SIZE = sizeof(data) * 3 / 2;
char file_buffer[FILE_BUFFER_SIZE];
StringFile *f = new_string_file(file_buffer, FILE_BUFFER_SIZE, 0, false, "w");

ASSERT_EQ(sizeof(data), f->write(data, sizeof(data)));
EXPECT_EQ(f->get_pos(), size_t(0)); // Data is buffered in the file stream
ASSERT_EQ(f->flush(), 0);
EXPECT_EQ(f->get_pos(), sizeof(data)); // Data should now be available
EXPECT_STREQ(f->get_str(), data);

f->reset();
ASSERT_EQ(f->get_pos(), size_t(0));
ASSERT_EQ(sizeof(data), f->write(data, sizeof(data)));
EXPECT_EQ(f->get_pos(), size_t(0)); // Data is buffered in the file stream
// The second write should trigger a buffer flush.
ASSERT_EQ(sizeof(data), f->write(data, sizeof(data)));
EXPECT_GE(f->get_pos(), size_t(0));
ASSERT_EQ(f->flush(), 0);
EXPECT_EQ(f->get_pos(), 2 * sizeof(data));

char read_data[sizeof(data)];
// This is not a readable file.
EXPECT_EQ(f->read(read_data, sizeof(data)), size_t(0));
EXPECT_TRUE(f->error());
EXPECT_NE(errno, 0);
errno = 0;

ASSERT_EQ(f->close(), 0);
}

TEST(LlvmLibcFileTest, ReadOnly) {
const char initial_content[] = "1234567890987654321";
constexpr size_t FILE_BUFFER_SIZE = sizeof(initial_content);
char file_buffer[FILE_BUFFER_SIZE];
StringFile *f = new_string_file(file_buffer, FILE_BUFFER_SIZE, 0, false, "r");
f->reset_and_fill(initial_content, sizeof(initial_content));

constexpr size_t READ_SIZE = sizeof(initial_content) / 2;
char read_data[READ_SIZE];
ASSERT_EQ(READ_SIZE, f->read(read_data, READ_SIZE));
EXPECT_FALSE(f->iseof());
// Reading less than file buffer worth will still read one
// full buffer worth of data.
EXPECT_STREQ(file_buffer, initial_content);
EXPECT_STREQ(file_buffer, f->get_str());
EXPECT_EQ(FILE_BUFFER_SIZE, f->get_pos());
// The read data should match what was supposed to be read anyway.
MemoryView src1(initial_content, READ_SIZE), dst1(read_data, READ_SIZE);
EXPECT_MEM_EQ(src1, dst1);

// Reading another buffer worth should read out everything in
// the file.
ASSERT_EQ(READ_SIZE, f->read(read_data, READ_SIZE));
EXPECT_FALSE(f->iseof());
MemoryView src2(initial_content + READ_SIZE, READ_SIZE),
dst2(read_data, READ_SIZE);
EXPECT_MEM_EQ(src2, dst2);

// Another read should trigger an EOF.
ASSERT_GT(READ_SIZE, f->read(read_data, READ_SIZE));
EXPECT_TRUE(f->iseof());

// Reset the pos to the beginning of the file which should allow
// reading again.
for (size_t i = 0; i < READ_SIZE; ++i)
read_data[i] = 0;
f->seek(0, SEEK_SET);
ASSERT_EQ(READ_SIZE, f->read(read_data, READ_SIZE));
MemoryView src3(initial_content, READ_SIZE), dst3(read_data, READ_SIZE);
EXPECT_MEM_EQ(src3, dst3);

// This is not a writable file.
EXPECT_EQ(f->write(initial_content, sizeof(initial_content)), size_t(0));
EXPECT_TRUE(f->error());
EXPECT_NE(errno, 0);
errno = 0;

ASSERT_EQ(f->close(), 0);
}

TEST(LlvmLibcFileTest, AppendOnly) {
const char initial_content[] = "1234567890987654321";
const char write_data[] = "append";
constexpr size_t FILE_BUFFER_SIZE = sizeof(write_data) * 3 / 2;
char file_buffer[FILE_BUFFER_SIZE];
StringFile *f = new_string_file(file_buffer, FILE_BUFFER_SIZE, 0, false, "a");
f->reset_and_fill(initial_content, sizeof(initial_content));

constexpr size_t READ_SIZE = 5;
char read_data[READ_SIZE];
// This is not a readable file.
ASSERT_EQ(f->read(read_data, READ_SIZE), size_t(0));
EXPECT_TRUE(f->error());
EXPECT_NE(errno, 0);
errno = 0;

// Write should succeed but will be buffered in the file stream.
ASSERT_EQ(f->write(write_data, sizeof(write_data)), sizeof(write_data));
EXPECT_EQ(f->get_pos(), size_t(0));
// Flushing will write to the file.
EXPECT_EQ(f->flush(), int(0));
EXPECT_EQ(f->get_pos(), sizeof(write_data) + sizeof(initial_content));

ASSERT_EQ(f->close(), 0);
}

TEST(LlvmLibcFileTest, WriteUpdate) {
const char data[] = "hello, file";
constexpr size_t FILE_BUFFER_SIZE = sizeof(data) * 3 / 2;
char file_buffer[FILE_BUFFER_SIZE];
StringFile *f =
new_string_file(file_buffer, FILE_BUFFER_SIZE, 0, false, "w+");

ASSERT_EQ(sizeof(data), f->write(data, sizeof(data)));
EXPECT_EQ(f->get_pos(), size_t(0)); // Data is buffered in the file stream

ASSERT_EQ(f->seek(0, SEEK_SET), 0);

// Seek flushes the stream buffer so we can read the previously written data.
char read_data[sizeof(data)];
ASSERT_EQ(f->read(read_data, sizeof(data)), sizeof(data));
EXPECT_STREQ(read_data, data);

ASSERT_EQ(f->close(), 0);
}

TEST(LlvmLibcFileTest, ReadUpdate) {
const char initial_content[] = "1234567890987654321";
constexpr size_t FILE_BUFFER_SIZE = sizeof(initial_content);
char file_buffer[FILE_BUFFER_SIZE];
StringFile *f =
new_string_file(file_buffer, FILE_BUFFER_SIZE, 0, false, "r+");
f->reset_and_fill(initial_content, sizeof(initial_content));

constexpr size_t READ_SIZE = sizeof(initial_content) / 2;
char read_data[READ_SIZE];
ASSERT_EQ(READ_SIZE, f->read(read_data, READ_SIZE));
EXPECT_FALSE(f->iseof());
// Reading less than file buffer worth will still read one
// full buffer worth of data.
EXPECT_STREQ(file_buffer, initial_content);
EXPECT_STREQ(file_buffer, f->get_str());
EXPECT_EQ(FILE_BUFFER_SIZE, f->get_pos());
// The read data should match what was supposed to be read anyway.
MemoryView src1(initial_content, READ_SIZE), dst1(read_data, READ_SIZE);
EXPECT_MEM_EQ(src1, dst1);

ASSERT_EQ(f->seek(0, SEEK_SET), 0);
const char write_data[] = "hello, file";
ASSERT_EQ(sizeof(write_data), f->write(write_data, sizeof(write_data)));
EXPECT_STREQ(file_buffer, write_data);
ASSERT_EQ(f->flush(), 0);
MemoryView dst2(f->get_str(), sizeof(write_data)),
src2(write_data, sizeof(write_data));
EXPECT_MEM_EQ(src2, dst2);

ASSERT_EQ(f->close(), 0);
}

TEST(LlvmLibcFileTest, AppendUpdate) {
const char initial_content[] = "1234567890987654321";
const char data[] = "hello, file";
constexpr size_t FILE_BUFFER_SIZE = sizeof(data) * 3 / 2;
char file_buffer[FILE_BUFFER_SIZE];
StringFile *f =
new_string_file(file_buffer, FILE_BUFFER_SIZE, 0, false, "a+");
f->reset_and_fill(initial_content, sizeof(initial_content));

ASSERT_EQ(sizeof(data), f->write(data, sizeof(data)));
EXPECT_EQ(f->get_pos(), size_t(0)); // Data is buffered in the file stream
ASSERT_EQ(f->flush(), 0);
// The flush should write |data| to the endof the file.
EXPECT_EQ(f->get_pos(), sizeof(data) + sizeof(initial_content));

ASSERT_EQ(f->seek(0, SEEK_SET), 0);
// Seeking to the beginning of the file should not affect the place
// where write happens.
ASSERT_EQ(sizeof(data), f->write(data, sizeof(data)));
ASSERT_EQ(f->flush(), 0);
EXPECT_EQ(f->get_pos(), sizeof(data) * 2 + sizeof(initial_content));
MemoryView src1(initial_content, sizeof(initial_content)),
dst1(f->get_str(), sizeof(initial_content));
EXPECT_MEM_EQ(src1, dst1);
MemoryView src2(data, sizeof(data)),
dst2(f->get_str() + sizeof(initial_content), sizeof(data));
EXPECT_MEM_EQ(src2, dst2);
MemoryView src3(data, sizeof(data)),
dst3(f->get_str() + sizeof(initial_content) + sizeof(data), sizeof(data));
EXPECT_MEM_EQ(src3, dst3);

// Reads can happen from any point.
ASSERT_EQ(f->seek(0, SEEK_SET), 0);
constexpr size_t READ_SIZE = 10;
char read_data[READ_SIZE];
ASSERT_EQ(READ_SIZE, f->read(read_data, READ_SIZE));
MemoryView src4(initial_content, READ_SIZE), dst4(read_data, READ_SIZE);
EXPECT_MEM_EQ(src4, dst4);

ASSERT_EQ(f->close(), 0);
}