tail: record file offset more carefully

* src/tail.c (struct File_spec): New member read_pos, replacing
size, since the value was really a read position not a size.
All uses changed.
(xlseek): Move defn up.
(record_open_fd): If the read_pos (formerly) size arg is unknown,
compute it here if it is a regular file.
(file_lines): Return the resulting read pos (or -1 on failure)
instead of storing it via a pointer.  Caller changed.
Simplify by using SEEK_CUR instead of SEEK_SET when that is easy.
Avoid reading the same data twice when there are not enough
lines in the file.
(pipe_lines): Return -2 on success, -1 on failure, rather than
updating a read pos via a pointer (which was weird for pipes anyway).
Caller changed.
(pipe_bytes, tail_bytes, tail_lines, tail):
Return -1 on failure, a file offset if known, and < -1 otherwise,
instead of storing a file offset via a pointer.  Caller changed.
(pipe_bytes): Take initial file offset as an arg, or -1 if unknown.
(start_bytes, start_lines): Return -1 (not 1) on error, -2 (not
-1) on EOF, and do not accept pointer to read pos as an arg since
neither we nor our caller know the read pos.  Callers changed.
(recheck): Do not assume a newly-opened file is at offset zero,
as this is not always true on Solaris.
(tail_forever, check_fspec): Use dump_remainder result only
on regular files, to prevent (very unlikely) overflow.
(tail_file): Remove no-longer-needed TAIL_TEST_SLEEP code.
This commit is contained in:
Paul Eggert
2025-07-31 18:45:12 -07:00
parent d3c7072a09
commit 976f8abc19

View File

@@ -124,12 +124,15 @@ struct File_spec
char const *prettyname;
/* Attributes of the file the last time we checked. */
off_t size;
struct timespec mtime;
dev_t dev;
ino_t ino;
mode_t mode;
/* If a regular file, the file's read position the last time we
checked. For non-regular files, the value is unspecified. */
off_t read_pos;
/* The specified name initially referred to a directory or some other
type for which tail isn't meaningful. Unlike for a permission problem
(tailable, below) once this is set, the name is not checked ever again. */
@@ -375,20 +378,47 @@ valid_file_spec (struct File_spec const *f)
return ((f->fd == -1) ^ (f->errnum == 0));
}
/* Record a file F with descriptor FD, size SIZE, status ST, and
blocking status BLOCKING. */
/* Call lseek with the specified arguments FD, OFFSET, WHENCE.
FD corresponds to PRETTYNAME.
Give a diagnostic and exit nonzero if lseek fails.
Otherwise, return the resulting offset. */
static off_t
xlseek (int fd, off_t offset, int whence, char const *prettyname)
{
off_t new_offset = lseek (fd, offset, whence);
if (0 <= new_offset)
return new_offset;
static char const *whence_msgid[] = {
[SEEK_SET] = N_("%s: cannot seek to offset %jd"),
[SEEK_CUR] = N_("%s: cannot seek to relative offset %jd"),
[SEEK_END] = N_("%s: cannot seek to end-relative offset %jd")
};
intmax_t joffset = offset;
error (EXIT_FAILURE, errno, gettext (whence_msgid[whence]),
quotef (prettyname), joffset);
}
/* Record a file F with descriptor FD, read position READ_POS, status ST,
and blocking status BLOCKING. READ_POS matters only for regular files,
and READ_POS < 0 means the position is unknown. */
static void
record_open_fd (struct File_spec *f, int fd,
off_t size, struct stat const *st,
off_t read_pos, struct stat const *st,
int blocking)
{
f->fd = fd;
f->size = size;
f->mtime = get_stat_mtime (st);
f->dev = st->st_dev;
f->ino = st->st_ino;
f->mode = st->st_mode;
if (S_ISREG (st->st_mode))
f->read_pos = (read_pos < 0
? xlseek (fd, 0, SEEK_CUR, f->prettyname)
: read_pos);
f->blocking = blocking;
f->n_unchanged_stats = 0;
f->ignore = false;
@@ -474,29 +504,6 @@ dump_remainder (bool want_header, char const *prettyname, int fd,
return n_read;
}
/* Call lseek with the specified arguments FD, OFFSET, WHENCE.
FD corresponds to PRETTYNAME.
Give a diagnostic and exit nonzero if lseek fails.
Otherwise, return the resulting offset. */
static off_t
xlseek (int fd, off_t offset, int whence, char const *prettyname)
{
off_t new_offset = lseek (fd, offset, whence);
if (0 <= new_offset)
return new_offset;
static char const *whence_msgid[] = {
[SEEK_SET] = N_("%s: cannot seek to offset %jd"),
[SEEK_CUR] = N_("%s: cannot seek to relative offset %jd"),
[SEEK_END] = N_("%s: cannot seek to end-relative offset %jd")
};
intmax_t joffset = offset;
error (EXIT_FAILURE, errno, gettext (whence_msgid[whence]),
quotef (prettyname), joffset);
}
/* Print the last N_LINES lines from the end of file FD.
Go backward through the file, reading 'BUFSIZ' bytes at a time (except
probably the first), until we hit the start of the file or have
@@ -504,20 +511,20 @@ xlseek (int fd, off_t offset, int whence, char const *prettyname)
START_POS is the starting position of the read pointer for the file
associated with FD (may be nonzero).
END_POS is the file offset of EOF (one larger than offset of last byte).
Return true if successful. */
The file is a regular file positioned at END_POS.
Return -1 if unsuccessful, otherwise the resulting file position,
which can be less than READ_POS if the file shrank. */
static bool
static off_t
file_lines (char const *prettyname, int fd, struct stat const *sb,
count_t n_lines, off_t start_pos, off_t end_pos,
count_t *read_pos)
count_t n_lines, off_t start_pos, off_t end_pos)
{
char *buffer;
idx_t bufsize = BUFSIZ;
off_t pos = end_pos;
bool ok = true;
if (n_lines == 0)
return true;
return pos;
/* Be careful with files with sizes that are a multiple of the page size,
as on /proc or /sys file systems these files accept seeking to within
@@ -541,16 +548,14 @@ file_lines (char const *prettyname, int fd, struct stat const *sb,
bytes_to_read = bufsize;
/* Make 'pos' a multiple of 'bufsize' (0 if the file is short), so that all
reads will be on block boundaries, which might increase efficiency. */
pos -= bytes_to_read;
xlseek (fd, pos, SEEK_SET, prettyname);
pos = xlseek (fd, -bytes_to_read, SEEK_CUR, prettyname);
ssize_t bytes_read = read (fd, buffer, bytes_to_read);
if (bytes_read < 0)
{
error (0, errno, _("error reading %s"), quoteaf (prettyname));
ok = false;
pos = -1;
goto free_buffer;
}
*read_pos = pos + bytes_read;
/* Count the incomplete line on files that don't end with a newline. */
if (bytes_read && buffer[bytes_read - 1] != line_end)
@@ -558,6 +563,8 @@ file_lines (char const *prettyname, int fd, struct stat const *sb,
do
{
pos += bytes_read;
/* Scan backward, counting the newlines in this bufferfull. */
idx_t n = bytes_read;
@@ -573,50 +580,44 @@ file_lines (char const *prettyname, int fd, struct stat const *sb,
/* If this newline isn't the last character in the buffer,
output the part that is after it. */
xwrite_stdout (nl + 1, bytes_read - (n + 1));
*read_pos += dump_remainder (false, prettyname, fd,
end_pos - (pos + bytes_read));
pos += dump_remainder (false, prettyname, fd, end_pos - pos);
goto free_buffer;
}
}
/* Not enough newlines in that bufferfull. */
if (pos == start_pos)
if (pos - bytes_read == start_pos)
{
/* Not enough lines in the file; print everything from
start_pos to the end. */
xlseek (fd, start_pos, SEEK_SET, prettyname);
*read_pos = start_pos + dump_remainder (false, prettyname, fd,
end_pos - start_pos);
xwrite_stdout (buffer, bytes_read);
dump_remainder (false, prettyname, fd, end_pos - pos);
goto free_buffer;
}
pos -= bufsize;
xlseek (fd, pos, SEEK_SET, prettyname);
pos = xlseek (fd, -bufsize, SEEK_CUR, prettyname);
bytes_read = read (fd, buffer, bufsize);
if (bytes_read < 0)
{
error (0, errno, _("error reading %s"), quoteaf (prettyname));
ok = false;
pos = -1;
goto free_buffer;
}
*read_pos = pos + bytes_read;
}
while (bytes_read > 0);
free_buffer:
free (buffer);
return ok;
return pos;
}
/* Print the last N_LINES lines from the end of the standard input,
open for reading as pipe FD.
Buffer the text as a linked list of LBUFFERs, adding them as needed.
Return true if successful. */
Return -2 if successful, -1 otherwise. */
static bool
pipe_lines (char const *prettyname, int fd, count_t n_lines,
count_t *read_pos)
static int
pipe_lines (char const *prettyname, int fd, count_t n_lines)
{
struct linebuffer
{
@@ -628,7 +629,7 @@ pipe_lines (char const *prettyname, int fd, count_t n_lines,
typedef struct linebuffer LBUFFER;
LBUFFER *first, *last, *tmp;
idx_t total_lines = 0; /* Total number of newlines in all buffers. */
bool ok = true;
int ok = -2;
ssize_t n_read; /* Size in bytes of most recent read */
first = last = xmalloc (sizeof (LBUFFER));
@@ -643,7 +644,6 @@ pipe_lines (char const *prettyname, int fd, count_t n_lines,
if (n_read <= 0)
break;
tmp->nbytes = n_read;
*read_pos += n_read;
tmp->nlines = 0;
tmp->next = nullptr;
@@ -692,7 +692,7 @@ pipe_lines (char const *prettyname, int fd, count_t n_lines,
if (n_read < 0 && errno != EAGAIN)
{
error (0, errno, _("error reading %s"), quoteaf (prettyname));
ok = false;
ok = -1;
goto free_lbuffers;
}
@@ -751,11 +751,13 @@ free_lbuffers:
/* Print the last N_BYTES characters from the end of FD.
Work even if the input is a pipe.
This is a stripped down version of pipe_lines.
Return true if successful. */
The initial file offset is READ_POS if nonnegative, otherwise unknown.
Return -1 if unsuccessful, otherwise the resulting file offset if known,
otherwise a value less than -1. */
static bool
static off_t
pipe_bytes (char const *prettyname, int fd, count_t n_bytes,
count_t *read_pos)
off_t read_pos)
{
struct charbuffer
{
@@ -767,9 +769,11 @@ pipe_bytes (char const *prettyname, int fd, count_t n_bytes,
CBUFFER *first, *last, *tmp;
idx_t i; /* Index into buffers. */
intmax_t total_bytes = 0; /* Total characters in all buffers. */
bool ok = true;
ssize_t n_read;
if (read_pos < 0)
read_pos = TYPE_MINIMUM (off_t);
first = last = xmalloc (sizeof (CBUFFER));
first->nbytes = 0;
first->next = nullptr;
@@ -781,7 +785,7 @@ pipe_bytes (char const *prettyname, int fd, count_t n_bytes,
n_read = read (fd, tmp->buffer, BUFSIZ);
if (n_read <= 0)
break;
*read_pos += n_read;
read_pos += n_read;
tmp->nbytes = n_read;
tmp->next = nullptr;
@@ -820,7 +824,7 @@ pipe_bytes (char const *prettyname, int fd, count_t n_bytes,
if (n_read < 0 && errno != EAGAIN)
{
error (0, errno, _("error reading %s"), quoteaf (prettyname));
ok = false;
read_pos = -1;
goto free_cbuffers;
}
@@ -847,16 +851,15 @@ free_cbuffers:
free (first);
first = tmp;
}
return ok;
return read_pos;
}
/* Skip N_BYTES characters from the start of pipe FD, and print
any extra characters that were read beyond that.
Return 1 on error, 0 if ok, -1 if EOF. */
Return -1 on error, 0 if ok, -2 if EOF. */
static int
start_bytes (char const *prettyname, int fd, count_t n_bytes,
count_t *read_pos)
start_bytes (char const *prettyname, int fd, count_t n_bytes)
{
char buffer[BUFSIZ];
@@ -864,13 +867,12 @@ start_bytes (char const *prettyname, int fd, count_t n_bytes,
{
ssize_t bytes_read = read (fd, buffer, BUFSIZ);
if (bytes_read == 0)
return -1;
return -2;
if (bytes_read < 0)
{
error (0, errno, _("error reading %s"), quoteaf (prettyname));
return 1;
return -1;
}
*read_pos += bytes_read;
if (bytes_read <= n_bytes)
n_bytes -= bytes_read;
else
@@ -884,12 +886,11 @@ start_bytes (char const *prettyname, int fd, count_t n_bytes,
}
/* Skip N_LINES lines at the start of file or pipe FD, and print
any extra characters that were read beyond that.
Return 1 on error, 0 if ok, -1 if EOF. */
any extra bytes that were read beyond that.
Return -1 on error, 0 if ok, -2 if EOF. */
static int
start_lines (char const *prettyname, int fd, count_t n_lines,
count_t *read_pos)
start_lines (char const *prettyname, int fd, count_t n_lines)
{
if (n_lines == 0)
return 0;
@@ -899,17 +900,14 @@ start_lines (char const *prettyname, int fd, count_t n_lines,
char buffer[BUFSIZ];
ssize_t bytes_read = read (fd, buffer, BUFSIZ);
if (bytes_read == 0) /* EOF */
return -1;
return -2;
if (bytes_read < 0) /* error */
{
error (0, errno, _("error reading %s"), quoteaf (prettyname));
return 1;
return -1;
}
char *buffer_end = buffer + bytes_read;
*read_pos += bytes_read;
char *p = buffer;
while ((p = memchr (p, line_end, buffer_end - p)))
{
@@ -1091,10 +1089,8 @@ recheck (struct File_spec *f, bool blocking)
data is written to a new file. */
if (new_file)
{
/* Start at the beginning of the file. */
record_open_fd (f, fd, 0, &new_stats, (is_stdin ? -1 : blocking));
if (S_ISREG (new_stats.st_mode))
xlseek (fd, 0, SEEK_SET, f->prettyname);
/* Start at the file's current position, normally the beginning. */
record_open_fd (f, fd, -1, &new_stats, is_stdin ? -1 : blocking);
}
}
@@ -1218,7 +1214,8 @@ tail_forever (struct File_spec *f, int n_files, double sleep_interval)
}
if (f[i].mode == stats.st_mode
&& (! S_ISREG (stats.st_mode) || f[i].size == stats.st_size)
&& (! S_ISREG (stats.st_mode)
|| f[i].read_pos == stats.st_size)
&& timespec_cmp (f[i].mtime, get_stat_mtime (&stats)) == 0)
{
if ((max_n_unchanged_stats_between_opens
@@ -1249,14 +1246,13 @@ tail_forever (struct File_spec *f, int n_files, double sleep_interval)
/* XXX: This is only a heuristic, as the file may have also
been truncated and written to if st_size >= size
(in which case we ignore new data <= size). */
if (S_ISREG (mode) && stats.st_size < f[i].size)
if (S_ISREG (mode) && stats.st_size < f[i].read_pos)
{
error (0, 0, _("%s: file truncated"),
quotef (prettyname));
/* Assume the file was truncated to 0,
and therefore output all "new" data. */
xlseek (fd, 0, SEEK_SET, prettyname);
f[i].size = 0;
f[i].read_pos = xlseek (fd, 0, SEEK_SET, prettyname);
}
if (i != last)
@@ -1274,17 +1270,19 @@ tail_forever (struct File_spec *f, int n_files, double sleep_interval)
if (f[i].blocking)
bytes_to_read = COPY_A_BUFFER;
else if (S_ISREG (mode) && f[i].remote)
bytes_to_read = stats.st_size - f[i].size;
bytes_to_read = stats.st_size - f[i].read_pos;
else
bytes_to_read = COPY_TO_EOF;
count_t bytes_read = dump_remainder (false, prettyname,
fd, bytes_to_read);
if (read_unchanged && bytes_read)
f[i].n_unchanged_stats = 0;
any_input |= (bytes_read != 0);
f[i].size += bytes_read;
count_t nr = dump_remainder (false, prettyname, fd, bytes_to_read);
if (0 < nr)
{
if (S_ISREG (mode))
f[i].read_pos += nr;
if (read_unchanged)
f[i].n_unchanged_stats = 0;
any_input = true;
}
}
if (! any_live_files (f, n_files))
@@ -1421,24 +1419,22 @@ check_fspec (struct File_spec *fspec, struct File_spec **prev_fspec)
(in which case we ignore new data <= size).
Though in the inotify case it's more likely we'll get
separate events for truncate() and write(). */
if (S_ISREG (fspec->mode) && stats.st_size < fspec->size)
if (S_ISREG (fspec->mode) && stats.st_size < fspec->read_pos)
{
error (0, 0, _("%s: file truncated"), quotef (prettyname));
xlseek (fspec->fd, 0, SEEK_SET, prettyname);
fspec->size = 0;
fspec->read_pos = xlseek (fspec->fd, 0, SEEK_SET, prettyname);
}
else if (S_ISREG (fspec->mode) && stats.st_size == fspec->size
else if (S_ISREG (fspec->mode) && stats.st_size == fspec->read_pos
&& timespec_cmp (fspec->mtime, get_stat_mtime (&stats)) == 0)
return;
bool want_header = print_headers && (fspec != *prev_fspec);
count_t bytes_read = dump_remainder (want_header, prettyname,
fspec->fd, COPY_TO_EOF);
fspec->size += bytes_read;
if (bytes_read)
count_t nr = dump_remainder (want_header, prettyname, fspec->fd, COPY_TO_EOF);
if (0 < nr)
{
if (S_ISREG (fspec->mode))
fspec->read_pos += nr;
*prev_fspec = fspec;
if (fflush (stdout) != 0)
write_error ();
@@ -1836,33 +1832,32 @@ get_file_status (struct File_spec *f, int fd, struct stat *st)
}
/* Output the last bytes of the file PRETTYNAME open for reading
in FD and with status ST. Output the last N_BYTES bytes, and set *READ_POS
to the resulting read position if the file is a regular file, and
to an unspecified value otherwise. Return true if and only if successful. */
in FD and with status ST. Output the last N_BYTES bytes.
Return -1 if unsuccessful, otherwise the resulting file offset if known,
otherwise a value less than -1. */
static bool
static off_t
tail_bytes (char const *prettyname, int fd, struct stat const *st,
count_t n_bytes, count_t *read_pos)
count_t n_bytes)
{
off_t current_pos
= (presume_input_pipe
? -1
: lseek (fd, from_start ? MIN (n_bytes, OFF_T_MAX) : 0, SEEK_CUR));
if (from_start)
{
off_t pos = (presume_input_pipe
? -1
: lseek (fd, MIN (n_bytes, OFF_T_MAX), SEEK_CUR));
if (0 <= pos)
*read_pos = pos;
else
if (current_pos < 0)
{
int t = start_bytes (prettyname, fd, n_bytes, read_pos);
if (t)
return t < 0;
int t = start_bytes (prettyname, fd, n_bytes);
if (t < 0)
return t;
}
n_bytes = COPY_TO_EOF;
}
else
{
off_t initial_pos = presume_input_pipe ? -1 : lseek (fd, 0, SEEK_CUR);
off_t current_pos = initial_pos;
off_t initial_pos = current_pos;
off_t end_pos = -1;
if (0 <= current_pos)
@@ -1904,47 +1899,45 @@ tail_bytes (char const *prettyname, int fd, struct stat const *st,
/* If the end is known and more than N_BYTES after the initial
position, go to N_BYTES before the end; otherwise go back to
the initial position. Set *READ_POS to the desired position,
and do not lseek if we cannot seek or are already at *READ_POS.
If the file is not seekable set *READ_POS = -1, which is OK
since callers use *READ_POS only for regular files. */
*read_pos = (initial_pos < end_pos && n_bytes < end_pos - initial_pos
the initial position. But do not lseek if lseek already failed. */
off_t pos = (initial_pos < end_pos && n_bytes < end_pos - initial_pos
? end_pos - n_bytes
: initial_pos);
if (*read_pos != current_pos)
xlseek (fd, *read_pos, SEEK_SET, prettyname);
if (pos != current_pos)
current_pos = xlseek (fd, pos, SEEK_SET, prettyname);
if (end_pos < 0)
return pipe_bytes (prettyname, fd, n_bytes, read_pos);
return pipe_bytes (prettyname, fd, n_bytes, current_pos);
}
*read_pos += dump_remainder (false, prettyname, fd, n_bytes);
return true;
count_t nr = dump_remainder (false, prettyname, fd, n_bytes);
return current_pos < 0 ? -2 : current_pos + nr;
}
/* Output the last lines of the file PRETTYNAME open for reading
in FD and with status ST. Output the last N_LINES lines, and set *READ_POS
to the resulting read position if the file is a regular file, and
to an unspecified value otherwise. Return true if and only if successful. */
in FD and with status ST. Output the last N_LINES lines.
Return -1 if unsuccessful, otherwise the resulting file offset if known,
otherwise -2. */
static bool
static off_t
tail_lines (char const *prettyname, int fd, struct stat const *st,
count_t n_lines, count_t *read_pos)
count_t n_lines)
{
if (from_start)
{
/* If skipping all input use lseek if possible, for speed. */
off_t pos;
if (OFF_T_MAX <= n_lines && 0 <= (pos = lseek (fd, 0, SEEK_END)))
*read_pos = pos;
else
if (OFF_T_MAX <= n_lines)
{
int t = start_lines (prettyname, fd, n_lines, read_pos);
if (t)
return t < 0;
*read_pos += dump_remainder (false, prettyname, fd, COPY_TO_EOF);
off_t e = lseek (fd, 0, SEEK_END);
if (0 <= e)
return e;
}
return true;
int t = start_lines (prettyname, fd, n_lines);
if (t)
return t;
dump_remainder (false, prettyname, fd, COPY_TO_EOF);
return -2;
}
else
{
@@ -1954,40 +1947,27 @@ tail_lines (char const *prettyname, int fd, struct stat const *st,
? lseek (fd, 0, SEEK_CUR)
: -1);
off_t end_pos = start_pos < 0 ? -1 : lseek (fd, 0, SEEK_END);
if (0 <= end_pos)
{
if (start_pos < end_pos)
return file_lines (prettyname, fd, st, n_lines,
start_pos, end_pos, read_pos);
/* Do not read from before the start offset, even if the
input file shrank. */
if (end_pos < start_pos)
xlseek (fd, start_pos, SEEK_SET, prettyname);
}
return pipe_lines (prettyname, fd, n_lines, read_pos);
return (end_pos < 0
? pipe_lines (prettyname, fd, n_lines)
: start_pos < end_pos
? file_lines (prettyname, fd, st, n_lines, start_pos, end_pos)
: start_pos != end_pos
? (/* Do not read from before the start offset,
even if the input file shrank. */
xlseek (fd, start_pos, SEEK_SET, prettyname))
: start_pos);
}
}
/* Display the last N_UNITS units of file FILENAME,
/* Display the last part of file FILENAME,
open for reading via FD and with status *ST.
Set *READ_POS to the position of the input stream pointer.
*READ_POS is usually the number of bytes read and corresponds to an
offset from the beginning of a file. However, it may be larger than
OFF_T_MAX (as for an input pipe), and may also be larger than the
number of bytes read (when an input pointer is initially not at
beginning of file), and may be far greater than the number of bytes
actually read for an input file that is seekable.
Return true if successful. */
Return -1 if unsuccessful, otherwise the resulting file offset if known,
otherwise a value less than -1. */
static bool
tail (char const *filename, int fd, struct stat const *st,
count_t n_units, count_t *read_pos)
static off_t
tail (char const *filename, int fd, struct stat const *st, count_t n_units)
{
*read_pos = 0;
return ((count_lines ? tail_lines : tail_bytes)
(filename, fd, st, n_units, read_pos));
return (count_lines ? tail_lines : tail_bytes) (filename, fd, st, n_units);
}
/* Display the last N_UNITS units of the file described by F.
@@ -2031,23 +2011,16 @@ tail_file (struct File_spec *f, count_t n_files, count_t n_units)
}
else
{
count_t read_pos;
if (print_headers)
write_header (f->prettyname);
struct stat stats;
bool stat_ok = get_file_status (f, fd, &stats);
ok = stat_ok && tail (f->prettyname, fd, &stats, n_units, &read_pos);
off_t read_pos = stat_ok ? tail (f->prettyname, fd, &stats, n_units) : -1;
ok = read_pos != -1;
if (forever)
{
#if TAIL_TEST_SLEEP
/* Before the tail function provided 'read_pos', there was
a race condition described in the URL below. This sleep
call made the window big enough to exercise the problem. */
xnanosleep (1);
#endif
if (stat_ok && !IS_TAILABLE_FILE_TYPE (stats.st_mode))
{
ok = false;
@@ -2071,9 +2044,6 @@ tail_file (struct File_spec *f, count_t n_files, count_t n_units)
}
else
{
/* Note: we must use read_pos here, not stats.st_size,
to avoid a race condition described by Ken Raeburn:
https://lists.gnu.org/r/bug-textutils/2003-05/msg00007.html */
record_open_fd (f, fd, read_pos, &stats, (is_stdin ? -1 : 1));
f->remote = fremote (fd, f);
}