aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/nvim/file.c36
-rw-r--r--src/nvim/file.h17
-rw-r--r--test/unit/file_spec.lua361
-rw-r--r--test/unit/helpers.lua5
-rw-r--r--test/unit/os/fs_spec.lua29
5 files changed, 404 insertions, 44 deletions
diff --git a/src/nvim/file.c b/src/nvim/file.c
index 2fb0e1cd87..2730e5b5f9 100644
--- a/src/nvim/file.c
+++ b/src/nvim/file.c
@@ -29,8 +29,6 @@
# include "file.c.generated.h"
#endif
-#define RWBUFSIZE (IOSIZE - 1)
-
/// Open file
///
/// @param[out] ret_fp Address where information needed for reading from or
@@ -80,7 +78,7 @@ int file_open(FileDescriptor *const ret_fp, const char *const fname,
ret_fp->wr = (wr == kTrue);
ret_fp->fd = fd;
ret_fp->eof = false;
- ret_fp->rv = rbuffer_new(RWBUFSIZE);
+ ret_fp->rv = rbuffer_new(kRWBufferSize);
ret_fp->_error = 0;
if (ret_fp->wr) {
ret_fp->rv->data = ret_fp;
@@ -162,7 +160,7 @@ int file_fsync(FileDescriptor *const fp)
/// Buffer used for writing
///
/// Like IObuff, but allows file_\* callers not to care about spoiling it.
-static char writebuf[RWBUFSIZE];
+static char writebuf[kRWBufferSize];
/// Function run when RBuffer is full when writing to a file
///
@@ -175,7 +173,10 @@ static void file_rb_write_full_cb(RBuffer *const rv, FileDescriptor *const fp)
{
assert(fp->wr);
assert(rv->data == (void *)fp);
- const size_t read_bytes = rbuffer_read(rv, writebuf, RWBUFSIZE);
+ if (rbuffer_size(rv) == 0) {
+ return;
+ }
+ const size_t read_bytes = rbuffer_read(rv, writebuf, kRWBufferSize);
const ptrdiff_t wres = os_write(fp->fd, writebuf, read_bytes);
if (wres != (ptrdiff_t)read_bytes) {
if (wres >= 0) {
@@ -215,22 +216,25 @@ ptrdiff_t file_read(FileDescriptor *const fp, char *const ret_buf,
if (read_remaining) {
assert(rbuffer_size(rv) == 0);
rbuffer_reset(rv);
- if (read_remaining >= RWBUFSIZE) {
+ if (read_remaining >= kRWBufferSize) {
#ifdef HAVE_READV
// If there is readv() syscall, then take an opportunity to populate
// both target buffer and RBuffer at once, …
- size_t read_count;
+ size_t write_count;
struct iovec iov[] = {
{ .iov_base = buf, .iov_len = read_remaining },
- { .iov_base = rbuffer_read_ptr(rv, &read_count), .iov_len = RWBUFSIZE
- },
+ { .iov_base = rbuffer_write_ptr(rv, &write_count),
+ .iov_len = kRWBufferSize },
};
- assert(read_count == RWBUFSIZE);
+ assert(write_count == kRWBufferSize);
const ptrdiff_t r_ret = os_readv(fp->fd, &fp->eof, iov,
ARRAY_SIZE(iov));
if (r_ret > 0) {
if (r_ret > (ptrdiff_t)read_remaining) {
+ read_remaining = 0;
rbuffer_produced(rv, (size_t)(r_ret - (ptrdiff_t)read_remaining));
+ } else {
+ read_remaining -= (size_t)r_ret;
}
} else if (r_ret < 0) {
return r_ret;
@@ -250,8 +254,8 @@ ptrdiff_t file_read(FileDescriptor *const fp, char *const ret_buf,
size_t write_count;
const ptrdiff_t r_ret = os_read(fp->fd, &fp->eof,
rbuffer_write_ptr(rv, &write_count),
- RWBUFSIZE);
- assert(write_count == RWBUFSIZE);
+ kRWBufferSize);
+ assert(write_count == kRWBufferSize);
if (r_ret > 0) {
rbuffer_produced(rv, (size_t)r_ret);
} else if (r_ret < 0) {
@@ -288,7 +292,7 @@ ptrdiff_t file_write(FileDescriptor *const fp, const char *const buf,
/// Buffer used for skipping. Its contents is undefined and should never be
/// used.
-static char skipbuf[RWBUFSIZE];
+static char skipbuf[kRWBufferSize];
/// Skip some bytes
///
@@ -300,10 +304,8 @@ ptrdiff_t file_skip(FileDescriptor *const fp, const size_t size)
assert(!fp->wr);
size_t read_bytes = 0;
do {
- ptrdiff_t new_read_bytes = file_read(
- fp, skipbuf, (size_t)(size - read_bytes > sizeof(skipbuf)
- ? sizeof(skipbuf)
- : size - read_bytes));
+ const ptrdiff_t new_read_bytes = file_read(
+ fp, skipbuf, MIN(size - read_bytes, sizeof(skipbuf)));
if (new_read_bytes < 0) {
return new_read_bytes;
} else if (new_read_bytes == 0) {
diff --git a/src/nvim/file.h b/src/nvim/file.h
index 5ee572750d..2ba415d2b9 100644
--- a/src/nvim/file.h
+++ b/src/nvim/file.h
@@ -32,6 +32,9 @@ typedef enum {
///< kFileCreateOnly.
} FileOpenFlags;
+static inline bool file_eof(const FileDescriptor *const fp)
+ REAL_FATTR_PURE REAL_FATTR_WARN_UNUSED_RESULT REAL_FATTR_NONNULL_ALL;
+
/// Check whether end of file was encountered
///
/// @param[in] fp File to check.
@@ -39,24 +42,30 @@ typedef enum {
/// @return true if it was, false if it was not or read operation was never
/// performed.
static inline bool file_eof(const FileDescriptor *const fp)
- FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
- FUNC_ATTR_ALWAYS_INLINE
{
return fp->eof && rbuffer_size(fp->rv) == 0;
}
+static inline int file_fd(const FileDescriptor *const fp)
+ REAL_FATTR_PURE REAL_FATTR_WARN_UNUSED_RESULT REAL_FATTR_NONNULL_ALL;
+
/// Return the file descriptor associated with the FileDescriptor structure
///
/// @param[in] fp File to check.
///
/// @return File descriptor.
static inline int file_fd(const FileDescriptor *const fp)
- FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
- FUNC_ATTR_ALWAYS_INLINE
{
return fp->fd;
}
+enum {
+ /// Read or write buffer size
+ ///
+ /// Currently equal to (IOSIZE - 1), but they do not need to be connected.
+ kRWBufferSize = 1024
+};
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "file.h.generated.h"
#endif
diff --git a/test/unit/file_spec.lua b/test/unit/file_spec.lua
new file mode 100644
index 0000000000..ac78dffe6e
--- /dev/null
+++ b/test/unit/file_spec.lua
@@ -0,0 +1,361 @@
+local lfs = require('lfs')
+
+local helpers = require('test.unit.helpers')
+
+local eq = helpers.eq
+local ok = helpers.ok
+local ffi = helpers.ffi
+local cimport = helpers.cimport
+
+local m = cimport('./src/nvim/file.h')
+
+local s = ''
+for i = 0, 255 do
+ s = s .. (i == 0 and '\0' or ('%c'):format(i))
+end
+local fcontents = s:rep(16)
+
+local dir = 'Xtest-unit-file_spec.d'
+local file1 = dir .. '/file1.dat'
+local file2 = dir .. '/file2.dat'
+local linkf = dir .. '/file.lnk'
+local linkb = dir .. '/broken.lnk'
+local filec = dir .. '/created-file.dat'
+
+before_each(function()
+ lfs.mkdir(dir);
+
+ local f1 = io.open(file1, 'w')
+ f1:write(fcontents)
+ f1:close()
+
+ local f2 = io.open(file2, 'w')
+ f2:write(fcontents)
+ f2:close()
+
+ lfs.link('file1.dat', linkf, true)
+ lfs.link('broken.dat', linkb, true)
+end)
+
+after_each(function()
+ os.remove(file1)
+ os.remove(file2)
+ os.remove(linkf)
+ os.remove(linkb)
+ os.remove(filec)
+ lfs.rmdir(dir)
+end)
+
+local function file_open(fname, flags, mode)
+ local ret2 = ffi.new('FileDescriptor')
+ local ret1 = m.file_open(ret2, fname, flags, mode)
+ return ret1, ret2
+end
+
+local function file_open_new(fname, flags, mode)
+ local ret1 = ffi.new('int[?]', 1, {0})
+ local ret2 = ffi.gc(m.file_open_new(ret1, fname, flags, mode), nil)
+ return ret1[0], ret2
+end
+
+local function file_write(fp, buf)
+ return m.file_write(fp, buf, #buf)
+end
+
+local function file_read(fp, size)
+ local buf = nil
+ if size == nil then
+ size = 0
+ else
+ buf = ffi.new('char[?]', size, ('\0'):rep(size))
+ end
+ local ret1 = m.file_read(fp, buf, size)
+ local ret2 = ''
+ if buf ~= nil then
+ ret2 = ffi.string(buf, size)
+ end
+ return ret1, ret2
+end
+
+local function file_fsync(fp)
+ return m.file_fsync(fp)
+end
+
+local function file_skip(fp, size)
+ return m.file_skip(fp, size)
+end
+
+describe('file_open', function()
+ it('can create a rwx------ file with kFileCreate', function()
+ local err, fp = file_open(filec, m.kFileCreate, 448)
+ eq(0, err)
+ local attrs = lfs.attributes(filec)
+ eq('rwx------', attrs.permissions)
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can create a rw------- file with kFileCreate', function()
+ local err, fp = file_open(filec, m.kFileCreate, 384)
+ eq(0, err)
+ local attrs = lfs.attributes(filec)
+ eq('rw-------', attrs.permissions)
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can create a rwx------ file with kFileCreateOnly', function()
+ local err, fp = file_open(filec, m.kFileCreateOnly, 448)
+ eq(0, err)
+ local attrs = lfs.attributes(filec)
+ eq('rwx------', attrs.permissions)
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can create a rw------- file with kFileCreateOnly', function()
+ local err, fp = file_open(filec, m.kFileCreateOnly, 384)
+ eq(0, err)
+ local attrs = lfs.attributes(filec)
+ eq('rw-------', attrs.permissions)
+ eq(0, m.file_close(fp))
+ end)
+
+ it('fails to open an existing file with kFileCreateOnly', function()
+ local err, fp = file_open(file1, m.kFileCreateOnly, 384)
+ eq(m.UV_EEXIST, err)
+ end)
+
+ it('fails to open an symlink with kFileNoSymlink', function()
+ local err, fp = file_open(linkf, m.kFileNoSymlink, 384)
+ eq(m.UV_ELOOP, err)
+ end)
+
+ it('can open an existing file write-only with kFileCreate', function()
+ local err, fp = file_open(file1, m.kFileCreate, 384)
+ eq(0, err)
+ eq(true, fp.wr)
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can open an existing file read-only with zero', function()
+ local err, fp = file_open(file1, 0, 384)
+ eq(0, err)
+ eq(false, fp.wr)
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can open an existing file read-only with kFileReadOnly', function()
+ local err, fp = file_open(file1, m.kFileReadOnly, 384)
+ eq(0, err)
+ eq(false, fp.wr)
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can open an existing file read-only with kFileNoSymlink', function()
+ local err, fp = file_open(file1, m.kFileNoSymlink, 384)
+ eq(0, err)
+ eq(false, fp.wr)
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can truncate an existing file with kFileTruncate', function()
+ local err, fp = file_open(file1, m.kFileTruncate, 384)
+ eq(0, err)
+ eq(true, fp.wr)
+ eq(0, m.file_close(fp))
+ local attrs = lfs.attributes(file1)
+ eq(0, attrs.size)
+ end)
+
+ it('can open an existing file write-only with kFileWriteOnly', function()
+ local err, fp = file_open(file1, m.kFileWriteOnly, 384)
+ eq(0, err)
+ eq(true, fp.wr)
+ eq(0, m.file_close(fp))
+ local attrs = lfs.attributes(file1)
+ eq(4096, attrs.size)
+ end)
+
+ it('fails to create a file with just kFileWriteOnly', function()
+ local err, fp = file_open(filec, m.kFileWriteOnly, 384)
+ eq(m.UV_ENOENT, err)
+ local attrs = lfs.attributes(filec)
+ eq(nil, attrs)
+ end)
+
+ it('can truncate an existing file with kFileTruncate when opening a symlink',
+ function()
+ local err, fp = file_open(linkf, m.kFileTruncate, 384)
+ eq(0, err)
+ eq(true, fp.wr)
+ eq(0, m.file_close(fp))
+ local attrs = lfs.attributes(file1)
+ eq(0, attrs.size)
+ end)
+
+ it('fails to open a directory write-only', function()
+ local err, fp = file_open(dir, m.kFileWriteOnly, 384)
+ eq(m.UV_EISDIR, err)
+ end)
+
+ it('fails to open a broken symbolic link write-only', function()
+ local err, fp = file_open(linkb, m.kFileWriteOnly, 384)
+ eq(m.UV_ENOENT, err)
+ end)
+
+ it('fails to open a broken symbolic link read-only', function()
+ local err, fp = file_open(linkb, m.kFileReadOnly, 384)
+ eq(m.UV_ENOENT, err)
+ end)
+end)
+
+describe('file_open_new', function()
+ it('can open a file read-only', function()
+ local err, fp = file_open_new(file1, 0, 384)
+ eq(0, err)
+ eq(false, fp.wr)
+ eq(0, m.file_free(fp))
+ end)
+
+ it('fails to open an existing file with kFileCreateOnly', function()
+ local err, fp = file_open_new(file1, m.kFileCreateOnly, 384)
+ eq(m.UV_EEXIST, err)
+ eq(nil, fp)
+ end)
+end)
+
+-- file_close is called above, so it is not tested directly
+
+describe('file_fsync', function()
+ it('can flush writes to disk', function()
+ local err, fp = file_open(filec, m.kFileCreateOnly, 384)
+ eq(0, file_fsync(fp))
+ eq(0, err)
+ eq(0, lfs.attributes(filec).size)
+ local wsize = file_write(fp, 'test')
+ eq(4, wsize)
+ eq(0, lfs.attributes(filec).size)
+ eq(0, file_fsync(fp))
+ eq(wsize, lfs.attributes(filec).size)
+ eq(0, m.file_close(fp))
+ end)
+end)
+
+describe('file_read', function()
+ it('can read small chunks of input until eof', function()
+ local err, fp = file_open(file1, 0, 384)
+ eq(0, err)
+ eq(false, fp.wr)
+ local shift = 0
+ while shift < #fcontents do
+ local size = 3
+ local exp_err = size
+ local exp_s = fcontents:sub(shift + 1, shift + size)
+ if shift + size >= #fcontents then
+ exp_err = #fcontents - shift
+ exp_s = (fcontents:sub(shift + 1, shift + size)
+ .. (('\0'):rep(size - exp_err)))
+ end
+ eq({exp_err, exp_s}, {file_read(fp, size)})
+ shift = shift + size
+ end
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can read the whole file at once', function()
+ local err, fp = file_open(file1, 0, 384)
+ eq(0, err)
+ eq(false, fp.wr)
+ eq({#fcontents, fcontents}, {file_read(fp, #fcontents)})
+ eq({0, ('\0'):rep(#fcontents)}, {file_read(fp, #fcontents)})
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can read more then 1024 bytes after reading a small chunk', function()
+ local err, fp = file_open(file1, 0, 384)
+ eq(0, err)
+ eq(false, fp.wr)
+ eq({5, fcontents:sub(1, 5)}, {file_read(fp, 5)})
+ eq({#fcontents - 5, fcontents:sub(6) .. (('\0'):rep(5))},
+ {file_read(fp, #fcontents)})
+ eq(0, m.file_close(fp))
+ end)
+
+ it('can read file by 768-byte-chunks', function()
+ local err, fp = file_open(file1, 0, 384)
+ eq(0, err)
+ eq(false, fp.wr)
+ local shift = 0
+ while shift < #fcontents do
+ local size = 768
+ local exp_err = size
+ local exp_s = fcontents:sub(shift + 1, shift + size)
+ if shift + size >= #fcontents then
+ exp_err = #fcontents - shift
+ exp_s = (fcontents:sub(shift + 1, shift + size)
+ .. (('\0'):rep(size - exp_err)))
+ end
+ eq({exp_err, exp_s}, {file_read(fp, size)})
+ shift = shift + size
+ end
+ eq(0, m.file_close(fp))
+ end)
+end)
+
+describe('file_write', function()
+ it('can write the whole file at once', function()
+ local err, fp = file_open(filec, m.kFileCreateOnly, 384)
+ eq(0, err)
+ eq(true, fp.wr)
+ local err = file_write(fp, fcontents)
+ eq(#fcontents, err)
+ eq(0, m.file_close(fp))
+ eq(err, lfs.attributes(filec).size)
+ eq(fcontents, io.open(filec):read('*a'))
+ end)
+
+ it('can write the whole file by small chunks', function()
+ local err, fp = file_open(filec, m.kFileCreateOnly, 384)
+ eq(0, err)
+ eq(true, fp.wr)
+ local shift = 0
+ while shift < #fcontents do
+ local size = 3
+ local s = fcontents:sub(shift + 1, shift + size)
+ local err = file_write(fp, s)
+ eq(err, #s)
+ shift = shift + size
+ end
+ eq(0, m.file_close(fp))
+ eq(#fcontents, lfs.attributes(filec).size)
+ eq(fcontents, io.open(filec):read('*a'))
+ end)
+
+ it('can write the whole file by 768-byte-chunks', function()
+ local err, fp = file_open(filec, m.kFileCreateOnly, 384)
+ eq(0, err)
+ eq(true, fp.wr)
+ local shift = 0
+ while shift < #fcontents do
+ local size = 768
+ local s = fcontents:sub(shift + 1, shift + size)
+ local err = file_write(fp, s)
+ eq(err, #s)
+ shift = shift + size
+ end
+ eq(0, m.file_close(fp))
+ eq(#fcontents, lfs.attributes(filec).size)
+ eq(fcontents, io.open(filec):read('*a'))
+ end)
+end)
+
+describe('file_skip', function()
+ it('can skip 3 bytes', function()
+ local err, fp = file_open(file1, 0, 384)
+ eq(0, err)
+ eq(false, fp.wr)
+ eq(3, file_skip(fp, 3))
+ local err, s = file_read(fp, 3)
+ eq(3, err)
+ eq(fcontents:sub(4, 6), s)
+ eq(0, m.file_close(fp))
+ end)
+end)
diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua
index f0c193884e..91da459393 100644
--- a/test/unit/helpers.lua
+++ b/test/unit/helpers.lua
@@ -7,6 +7,7 @@ local global_helpers = require('test.helpers')
local neq = global_helpers.neq
local eq = global_helpers.eq
+local ok = global_helpers.ok
-- add some standard header locations
for _, p in ipairs(Paths.include_paths) do
@@ -34,7 +35,8 @@ local function filter_complex_blocks(body)
if not (string.find(line, "(^)", 1, true) ~= nil
or string.find(line, "_ISwupper", 1, true)
or string.find(line, "msgpack_zone_push_finalizer")
- or string.find(line, "msgpack_unpacker_reserve_buffer")) then
+ or string.find(line, "msgpack_unpacker_reserve_buffer")
+ or string.find(line, "inline _Bool")) then
result[#result + 1] = line
end
end
@@ -156,6 +158,7 @@ return {
cimport = cimport,
cppimport = cppimport,
internalize = internalize,
+ ok = ok,
eq = eq,
neq = neq,
ffi = ffi,
diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua
index d7e98b43ce..1f7f6fb791 100644
--- a/test/unit/os/fs_spec.lua
+++ b/test/unit/os/fs_spec.lua
@@ -6,6 +6,7 @@ local helpers = require('test.unit.helpers')
local cimport = helpers.cimport
local cppimport = helpers.cppimport
local internalize = helpers.internalize
+local ok = helpers.ok
local eq = helpers.eq
local neq = helpers.neq
local ffi = helpers.ffi
@@ -17,10 +18,6 @@ local NULL = helpers.NULL
local NODE_NORMAL = 0
local NODE_WRITABLE = 1
-local function ok(expr)
- assert.is_true(expr)
-end
-
cimport('unistd.h')
cimport('./src/nvim/os/shell.h')
cimport('./src/nvim/option_defs.h')
@@ -31,6 +28,12 @@ cppimport('sys/stat.h')
cppimport('fcntl.h')
cppimport('uv-errno.h')
+local s = ''
+for i = 0, 255 do
+ s = s .. (i == 0 and '\0' or ('%c'):format(i))
+end
+local fcontents = s:rep(16)
+
local buffer = ""
local directory = nil
local absolute_executable = nil
@@ -547,12 +550,6 @@ describe('fs function', function()
describe('os_read', function()
local file = 'test-unit-os-fs_spec-os_read.dat'
- local s = ''
- for i = 0, 255 do
- s = s .. (i == 0 and '\0' or ('%c'):format(i))
- end
- local fcontents = s:rep(16)
-
before_each(function()
local f = io.open(file, 'w')
f:write(fcontents)
@@ -607,12 +604,6 @@ describe('fs function', function()
end
local file = 'test-unit-os-fs_spec-os_readv.dat'
- local s = ''
- for i = 0, 255 do
- s = s .. (i == 0 and '\0' or ('%c'):format(i))
- end
- local fcontents = s:rep(16)
-
before_each(function()
local f = io.open(file, 'w')
f:write(fcontents)
@@ -673,12 +664,6 @@ describe('fs function', function()
-- Function may be absent
local file = 'test-unit-os-fs_spec-os_write.dat'
- local s = ''
- for i = 0, 255 do
- s = s .. (i == 0 and '\0' or ('%c'):format(i))
- end
- local fcontents = s:rep(16)
-
before_each(function()
local f = io.open(file, 'w')
f:write(fcontents)