c42a19d5e3
2005-05-12 Thomas Koenig <Thomas.Koenig@online.de> PR libfortran/21324 * runtime/memory.c: Don't define GFC_CLEAR_MEMORY (it's a performance hog). * io/open.c (new_unit): Zero freshly allocated memory for unit structure. * io/unit.c (init_units): Zero freshly allocated memory for STDIN, STDOUT and STDERR. * io/unix.c (open_internal): Zero freshly allocated memory for unix_stream. (fd_to_stream): Likewise. From-SVN: r99619
1567 lines
34 KiB
C
1567 lines
34 KiB
C
/* Copyright (C) 2002, 2003, 2004 Free Software Foundation, Inc.
|
|
Contributed by Andy Vaught
|
|
|
|
This file is part of the GNU Fortran 95 runtime library (libgfortran).
|
|
|
|
Libgfortran is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2, or (at your option)
|
|
any later version.
|
|
|
|
In addition to the permissions in the GNU General Public License, the
|
|
Free Software Foundation gives you unlimited permission to link the
|
|
compiled version of this file into combinations with other programs,
|
|
and to distribute those combinations without any restriction coming
|
|
from the use of this file. (The General Public License restrictions
|
|
do apply in other respects; for example, they cover modification of
|
|
the file, and distribution when not linked into a combine
|
|
executable.)
|
|
|
|
Libgfortran is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Libgfortran; see the file COPYING. If not, write to
|
|
the Free Software Foundation, 59 Temple Place - Suite 330,
|
|
Boston, MA 02111-1307, USA. */
|
|
|
|
/* Unix stream I/O module */
|
|
|
|
#include "config.h"
|
|
#include <stdlib.h>
|
|
#include <limits.h>
|
|
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
|
|
#ifdef HAVE_SYS_MMAN_H
|
|
#include <sys/mman.h>
|
|
#endif
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
|
|
#include "libgfortran.h"
|
|
#include "io.h"
|
|
|
|
#ifndef PATH_MAX
|
|
#define PATH_MAX 1024
|
|
#endif
|
|
|
|
#ifndef MAP_FAILED
|
|
#define MAP_FAILED ((void *) -1)
|
|
#endif
|
|
|
|
#ifndef PROT_READ
|
|
#define PROT_READ 1
|
|
#endif
|
|
|
|
#ifndef PROT_WRITE
|
|
#define PROT_WRITE 2
|
|
#endif
|
|
|
|
/* These flags aren't defined on all targets (mingw32), so provide them
|
|
here. */
|
|
#ifndef S_IRGRP
|
|
#define S_IRGRP 0
|
|
#endif
|
|
|
|
#ifndef S_IWGRP
|
|
#define S_IWGRP 0
|
|
#endif
|
|
|
|
#ifndef S_IROTH
|
|
#define S_IROTH 0
|
|
#endif
|
|
|
|
#ifndef S_IWOTH
|
|
#define S_IWOTH 0
|
|
#endif
|
|
|
|
/* This implementation of stream I/O is based on the paper:
|
|
*
|
|
* "Exploiting the advantages of mapped files for stream I/O",
|
|
* O. Krieger, M. Stumm and R. Umrau, "Proceedings of the 1992 Winter
|
|
* USENIX conference", p. 27-42.
|
|
*
|
|
* It differs in a number of ways from the version described in the
|
|
* paper. First of all, threads are not an issue during I/O and we
|
|
* also don't have to worry about having multiple regions, since
|
|
* fortran's I/O model only allows you to be one place at a time.
|
|
*
|
|
* On the other hand, we have to be able to writing at the end of a
|
|
* stream, read from the start of a stream or read and write blocks of
|
|
* bytes from an arbitrary position. After opening a file, a pointer
|
|
* to a stream structure is returned, which is used to handle file
|
|
* accesses until the file is closed.
|
|
*
|
|
* salloc_at_r(stream, len, where)-- Given a stream pointer, return a
|
|
* pointer to a block of memory that mirror the file at position
|
|
* 'where' that is 'len' bytes long. The len integer is updated to
|
|
* reflect how many bytes were actually read. The only reason for a
|
|
* short read is end of file. The file pointer is updated. The
|
|
* pointer is valid until the next call to salloc_*.
|
|
*
|
|
* salloc_at_w(stream, len, where)-- Given the stream pointer, returns
|
|
* a pointer to a block of memory that is updated to reflect the state
|
|
* of the file. The length of the buffer is always equal to that
|
|
* requested. The buffer must be completely set by the caller. When
|
|
* data has been written, the sfree() function must be called to
|
|
* indicate that the caller is done writing data to the buffer. This
|
|
* may or may not cause a physical write.
|
|
*
|
|
* Short forms of these are salloc_r() and salloc_w() which drop the
|
|
* 'where' parameter and use the current file pointer. */
|
|
|
|
|
|
#define BUFFER_SIZE 8192
|
|
|
|
typedef struct
|
|
{
|
|
stream st;
|
|
|
|
int fd;
|
|
gfc_offset buffer_offset; /* File offset of the start of the buffer */
|
|
gfc_offset physical_offset; /* Current physical file offset */
|
|
gfc_offset logical_offset; /* Current logical file offset */
|
|
gfc_offset dirty_offset; /* Start of modified bytes in buffer */
|
|
gfc_offset file_length; /* Length of the file, -1 if not seekable. */
|
|
|
|
char *buffer;
|
|
int len; /* Physical length of the current buffer */
|
|
int active; /* Length of valid bytes in the buffer */
|
|
|
|
int prot;
|
|
int ndirty; /* Dirty bytes starting at dirty_offset */
|
|
|
|
unsigned unbuffered:1, mmaped:1;
|
|
|
|
char small_buffer[BUFFER_SIZE];
|
|
|
|
}
|
|
unix_stream;
|
|
|
|
/*move_pos_offset()-- Move the record pointer right or left
|
|
*relative to current position */
|
|
|
|
int
|
|
move_pos_offset (stream* st, int pos_off)
|
|
{
|
|
unix_stream * str = (unix_stream*)st;
|
|
if (pos_off < 0)
|
|
{
|
|
str->active += pos_off;
|
|
if (str->active < 0)
|
|
str->active = 0;
|
|
|
|
str->logical_offset += pos_off;
|
|
|
|
if (str->dirty_offset+str->ndirty > str->logical_offset)
|
|
{
|
|
if (str->ndirty + pos_off > 0)
|
|
str->ndirty += pos_off ;
|
|
else
|
|
{
|
|
str->dirty_offset += pos_off + pos_off;
|
|
str->ndirty = 0 ;
|
|
}
|
|
}
|
|
|
|
return pos_off ;
|
|
}
|
|
return 0 ;
|
|
}
|
|
|
|
|
|
/* fix_fd()-- Given a file descriptor, make sure it is not one of the
|
|
* standard descriptors, returning a non-standard descriptor. If the
|
|
* user specifies that system errors should go to standard output,
|
|
* then closes standard output, we don't want the system errors to a
|
|
* file that has been given file descriptor 1 or 0. We want to send
|
|
* the error to the invalid descriptor. */
|
|
|
|
static int
|
|
fix_fd (int fd)
|
|
{
|
|
int input, output, error;
|
|
|
|
input = output = error = 0;
|
|
|
|
/* Unix allocates the lowest descriptors first, so a loop is not
|
|
required, but this order is. */
|
|
|
|
if (fd == STDIN_FILENO)
|
|
{
|
|
fd = dup (fd);
|
|
input = 1;
|
|
}
|
|
if (fd == STDOUT_FILENO)
|
|
{
|
|
fd = dup (fd);
|
|
output = 1;
|
|
}
|
|
if (fd == STDERR_FILENO)
|
|
{
|
|
fd = dup (fd);
|
|
error = 1;
|
|
}
|
|
|
|
if (input)
|
|
close (STDIN_FILENO);
|
|
if (output)
|
|
close (STDOUT_FILENO);
|
|
if (error)
|
|
close (STDERR_FILENO);
|
|
|
|
return fd;
|
|
}
|
|
|
|
|
|
/* write()-- Write a buffer to a descriptor, allowing for short writes */
|
|
|
|
static int
|
|
writen (int fd, char *buffer, int len)
|
|
{
|
|
int n, n0;
|
|
|
|
n0 = len;
|
|
|
|
while (len > 0)
|
|
{
|
|
n = write (fd, buffer, len);
|
|
if (n < 0)
|
|
return n;
|
|
|
|
buffer += n;
|
|
len -= n;
|
|
}
|
|
|
|
return n0;
|
|
}
|
|
|
|
|
|
#if 0
|
|
/* readn()-- Read bytes into a buffer, allowing for short reads. If
|
|
* fewer than len bytes are returned, it is because we've hit the end
|
|
* of file. */
|
|
|
|
static int
|
|
readn (int fd, char *buffer, int len)
|
|
{
|
|
int nread, n;
|
|
|
|
nread = 0;
|
|
|
|
while (len > 0)
|
|
{
|
|
n = read (fd, buffer, len);
|
|
if (n < 0)
|
|
return n;
|
|
|
|
if (n == 0)
|
|
return nread;
|
|
|
|
buffer += n;
|
|
nread += n;
|
|
len -= n;
|
|
}
|
|
|
|
return nread;
|
|
}
|
|
#endif
|
|
|
|
|
|
/* get_oserror()-- Get the most recent operating system error. For
|
|
* unix, this is errno. */
|
|
|
|
const char *
|
|
get_oserror (void)
|
|
{
|
|
return strerror (errno);
|
|
}
|
|
|
|
|
|
/* sys_exit()-- Terminate the program with an exit code */
|
|
|
|
void
|
|
sys_exit (int code)
|
|
{
|
|
exit (code);
|
|
}
|
|
|
|
|
|
/*********************************************************************
|
|
File descriptor stream functions
|
|
*********************************************************************/
|
|
|
|
/* fd_flush()-- Write bytes that need to be written */
|
|
|
|
static try
|
|
fd_flush (unix_stream * s)
|
|
{
|
|
if (s->ndirty == 0)
|
|
return SUCCESS;;
|
|
|
|
if (s->physical_offset != s->dirty_offset &&
|
|
lseek (s->fd, s->dirty_offset, SEEK_SET) < 0)
|
|
return FAILURE;
|
|
|
|
if (writen (s->fd, s->buffer + (s->dirty_offset - s->buffer_offset),
|
|
s->ndirty) < 0)
|
|
return FAILURE;
|
|
|
|
s->physical_offset = s->dirty_offset + s->ndirty;
|
|
|
|
/* don't increment file_length if the file is non-seekable */
|
|
if (s->file_length != -1 && s->physical_offset > s->file_length)
|
|
s->file_length = s->physical_offset;
|
|
s->ndirty = 0;
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
/* fd_alloc()-- Arrange a buffer such that the salloc() request can be
|
|
* satisfied. This subroutine gets the buffer ready for whatever is
|
|
* to come next. */
|
|
|
|
static void
|
|
fd_alloc (unix_stream * s, gfc_offset where, int *len)
|
|
{
|
|
char *new_buffer;
|
|
int n, read_len;
|
|
|
|
if (*len <= BUFFER_SIZE)
|
|
{
|
|
new_buffer = s->small_buffer;
|
|
read_len = BUFFER_SIZE;
|
|
}
|
|
else
|
|
{
|
|
new_buffer = get_mem (*len);
|
|
read_len = *len;
|
|
}
|
|
|
|
/* Salvage bytes currently within the buffer. This is important for
|
|
* devices that cannot seek. */
|
|
|
|
if (s->buffer != NULL && s->buffer_offset <= where &&
|
|
where <= s->buffer_offset + s->active)
|
|
{
|
|
|
|
n = s->active - (where - s->buffer_offset);
|
|
memmove (new_buffer, s->buffer + (where - s->buffer_offset), n);
|
|
|
|
s->active = n;
|
|
}
|
|
else
|
|
{ /* new buffer starts off empty */
|
|
s->active = 0;
|
|
}
|
|
|
|
s->buffer_offset = where;
|
|
|
|
/* free the old buffer if necessary */
|
|
|
|
if (s->buffer != NULL && s->buffer != s->small_buffer)
|
|
free_mem (s->buffer);
|
|
|
|
s->buffer = new_buffer;
|
|
s->len = read_len;
|
|
s->mmaped = 0;
|
|
}
|
|
|
|
|
|
/* fd_alloc_r_at()-- Allocate a stream buffer for reading. Either
|
|
* we've already buffered the data or we need to load it. Returns
|
|
* NULL on I/O error. */
|
|
|
|
static char *
|
|
fd_alloc_r_at (unix_stream * s, int *len, gfc_offset where)
|
|
{
|
|
gfc_offset m;
|
|
int n;
|
|
|
|
if (where == -1)
|
|
where = s->logical_offset;
|
|
|
|
if (s->buffer != NULL && s->buffer_offset <= where &&
|
|
where + *len <= s->buffer_offset + s->active)
|
|
{
|
|
|
|
/* Return a position within the current buffer */
|
|
|
|
s->logical_offset = where + *len;
|
|
return s->buffer + where - s->buffer_offset;
|
|
}
|
|
|
|
fd_alloc (s, where, len);
|
|
|
|
m = where + s->active;
|
|
|
|
if (s->physical_offset != m && lseek (s->fd, m, SEEK_SET) < 0)
|
|
return NULL;
|
|
|
|
n = read (s->fd, s->buffer + s->active, s->len - s->active);
|
|
if (n < 0)
|
|
return NULL;
|
|
|
|
s->physical_offset = where + n;
|
|
|
|
s->active += n;
|
|
if (s->active < *len)
|
|
*len = s->active; /* Bytes actually available */
|
|
|
|
s->logical_offset = where + *len;
|
|
|
|
return s->buffer;
|
|
}
|
|
|
|
|
|
/* fd_alloc_w_at()-- Allocate a stream buffer for writing. Either
|
|
* we've already buffered the data or we need to load it. */
|
|
|
|
static char *
|
|
fd_alloc_w_at (unix_stream * s, int *len, gfc_offset where)
|
|
{
|
|
gfc_offset n;
|
|
|
|
if (where == -1)
|
|
where = s->logical_offset;
|
|
|
|
if (s->buffer == NULL || s->buffer_offset > where ||
|
|
where + *len > s->buffer_offset + s->len)
|
|
{
|
|
|
|
if (fd_flush (s) == FAILURE)
|
|
return NULL;
|
|
fd_alloc (s, where, len);
|
|
}
|
|
|
|
/* Return a position within the current buffer */
|
|
if (s->ndirty == 0
|
|
|| where > s->dirty_offset + s->ndirty
|
|
|| s->dirty_offset > where + *len)
|
|
{ /* Discontiguous blocks, start with a clean buffer. */
|
|
/* Flush the buffer. */
|
|
if (s->ndirty != 0)
|
|
fd_flush (s);
|
|
s->dirty_offset = where;
|
|
s->ndirty = *len;
|
|
}
|
|
else
|
|
{
|
|
gfc_offset start; /* Merge with the existing data. */
|
|
if (where < s->dirty_offset)
|
|
start = where;
|
|
else
|
|
start = s->dirty_offset;
|
|
if (where + *len > s->dirty_offset + s->ndirty)
|
|
s->ndirty = where + *len - start;
|
|
else
|
|
s->ndirty = s->dirty_offset + s->ndirty - start;
|
|
s->dirty_offset = start;
|
|
}
|
|
|
|
s->logical_offset = where + *len;
|
|
|
|
if (where + *len > s->file_length)
|
|
s->file_length = where + *len;
|
|
|
|
n = s->logical_offset - s->buffer_offset;
|
|
if (n > s->active)
|
|
s->active = n;
|
|
|
|
return s->buffer + where - s->buffer_offset;
|
|
}
|
|
|
|
|
|
static try
|
|
fd_sfree (unix_stream * s)
|
|
{
|
|
if (s->ndirty != 0 &&
|
|
(s->buffer != s->small_buffer || options.all_unbuffered ||
|
|
s->unbuffered))
|
|
return fd_flush (s);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
static int
|
|
fd_seek (unix_stream * s, gfc_offset offset)
|
|
{
|
|
s->physical_offset = s->logical_offset = offset;
|
|
|
|
return (lseek (s->fd, offset, SEEK_SET) < 0) ? FAILURE : SUCCESS;
|
|
}
|
|
|
|
|
|
/* truncate_file()-- Given a unit, truncate the file at the current
|
|
* position. Sets the physical location to the new end of the file.
|
|
* Returns nonzero on error. */
|
|
|
|
static try
|
|
fd_truncate (unix_stream * s)
|
|
{
|
|
if (lseek (s->fd, s->logical_offset, SEEK_SET) == -1)
|
|
return FAILURE;
|
|
|
|
/* non-seekable files, like terminals and fifo's fail the lseek.
|
|
the fd is a regular file at this point */
|
|
|
|
#ifdef HAVE_FTRUNCATE
|
|
if (ftruncate (s->fd, s->logical_offset))
|
|
#else
|
|
#ifdef HAVE_CHSIZE
|
|
if (chsize (s->fd, s->logical_offset))
|
|
#endif
|
|
#endif
|
|
{
|
|
s->physical_offset = s->file_length = 0;
|
|
return FAILURE;
|
|
}
|
|
|
|
s->physical_offset = s->file_length = s->logical_offset;
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
static try
|
|
fd_close (unix_stream * s)
|
|
{
|
|
if (fd_flush (s) == FAILURE)
|
|
return FAILURE;
|
|
|
|
if (s->buffer != NULL && s->buffer != s->small_buffer)
|
|
free_mem (s->buffer);
|
|
|
|
if (close (s->fd) < 0)
|
|
return FAILURE;
|
|
|
|
free_mem (s);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
static void
|
|
fd_open (unix_stream * s)
|
|
{
|
|
if (isatty (s->fd))
|
|
s->unbuffered = 1;
|
|
|
|
s->st.alloc_r_at = (void *) fd_alloc_r_at;
|
|
s->st.alloc_w_at = (void *) fd_alloc_w_at;
|
|
s->st.sfree = (void *) fd_sfree;
|
|
s->st.close = (void *) fd_close;
|
|
s->st.seek = (void *) fd_seek;
|
|
s->st.truncate = (void *) fd_truncate;
|
|
|
|
s->buffer = NULL;
|
|
}
|
|
|
|
|
|
/*********************************************************************
|
|
mmap stream functions
|
|
|
|
Because mmap() is not capable of extending a file, we have to keep
|
|
track of how long the file is. We also have to be able to detect end
|
|
of file conditions. If there are multiple writers to the file (which
|
|
can only happen outside the current program), things will get
|
|
confused. Then again, things will get confused anyway.
|
|
|
|
*********************************************************************/
|
|
|
|
#if HAVE_MMAP
|
|
|
|
static int page_size, page_mask;
|
|
|
|
/* mmap_flush()-- Deletes a memory mapping if something is mapped. */
|
|
|
|
static try
|
|
mmap_flush (unix_stream * s)
|
|
{
|
|
if (!s->mmaped)
|
|
return fd_flush (s);
|
|
|
|
if (s->buffer == NULL)
|
|
return SUCCESS;
|
|
|
|
if (munmap (s->buffer, s->active))
|
|
return FAILURE;
|
|
|
|
s->buffer = NULL;
|
|
s->active = 0;
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
/* mmap_alloc()-- mmap() a section of the file. The whole section is
|
|
* guaranteed to be mappable. */
|
|
|
|
static try
|
|
mmap_alloc (unix_stream * s, gfc_offset where, int *len)
|
|
{
|
|
gfc_offset offset;
|
|
int length;
|
|
char *p;
|
|
|
|
if (mmap_flush (s) == FAILURE)
|
|
return FAILURE;
|
|
|
|
offset = where & page_mask; /* Round down to the next page */
|
|
|
|
length = ((where - offset) & page_mask) + 2 * page_size;
|
|
|
|
p = mmap (NULL, length, s->prot, MAP_SHARED, s->fd, offset);
|
|
if (p == (char *) MAP_FAILED)
|
|
return FAILURE;
|
|
|
|
s->mmaped = 1;
|
|
s->buffer = p;
|
|
s->buffer_offset = offset;
|
|
s->active = length;
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
static char *
|
|
mmap_alloc_r_at (unix_stream * s, int *len, gfc_offset where)
|
|
{
|
|
gfc_offset m;
|
|
|
|
if (where == -1)
|
|
where = s->logical_offset;
|
|
|
|
m = where + *len;
|
|
|
|
if ((s->buffer == NULL || s->buffer_offset > where ||
|
|
m > s->buffer_offset + s->active) &&
|
|
mmap_alloc (s, where, len) == FAILURE)
|
|
return NULL;
|
|
|
|
if (m > s->file_length)
|
|
{
|
|
*len = s->file_length - s->logical_offset;
|
|
s->logical_offset = s->file_length;
|
|
}
|
|
else
|
|
s->logical_offset = m;
|
|
|
|
return s->buffer + (where - s->buffer_offset);
|
|
}
|
|
|
|
|
|
static char *
|
|
mmap_alloc_w_at (unix_stream * s, int *len, gfc_offset where)
|
|
{
|
|
if (where == -1)
|
|
where = s->logical_offset;
|
|
|
|
/* If we're extending the file, we have to use file descriptor
|
|
* methods. */
|
|
|
|
if (where + *len > s->file_length)
|
|
{
|
|
if (s->mmaped)
|
|
mmap_flush (s);
|
|
return fd_alloc_w_at (s, len, where);
|
|
}
|
|
|
|
if ((s->buffer == NULL || s->buffer_offset > where ||
|
|
where + *len > s->buffer_offset + s->active ||
|
|
where < s->buffer_offset + s->active) &&
|
|
mmap_alloc (s, where, len) == FAILURE)
|
|
return NULL;
|
|
|
|
s->logical_offset = where + *len;
|
|
|
|
return s->buffer + where - s->buffer_offset;
|
|
}
|
|
|
|
|
|
static int
|
|
mmap_seek (unix_stream * s, gfc_offset offset)
|
|
{
|
|
s->logical_offset = offset;
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
static try
|
|
mmap_close (unix_stream * s)
|
|
{
|
|
try t;
|
|
|
|
t = mmap_flush (s);
|
|
|
|
if (close (s->fd) < 0)
|
|
t = FAILURE;
|
|
free_mem (s);
|
|
|
|
return t;
|
|
}
|
|
|
|
|
|
static try
|
|
mmap_sfree (unix_stream * s)
|
|
{
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
/* mmap_open()-- mmap_specific open. If the particular file cannot be
|
|
* mmap()-ed, we fall back to the file descriptor functions. */
|
|
|
|
static try
|
|
mmap_open (unix_stream * s)
|
|
{
|
|
char *p;
|
|
int i;
|
|
|
|
page_size = getpagesize ();
|
|
page_mask = ~0;
|
|
|
|
p = mmap (0, page_size, s->prot, MAP_SHARED, s->fd, 0);
|
|
if (p == (char *) MAP_FAILED)
|
|
{
|
|
fd_open (s);
|
|
return SUCCESS;
|
|
}
|
|
|
|
munmap (p, page_size);
|
|
|
|
i = page_size >> 1;
|
|
while (i != 0)
|
|
{
|
|
page_mask <<= 1;
|
|
i >>= 1;
|
|
}
|
|
|
|
s->st.alloc_r_at = (void *) mmap_alloc_r_at;
|
|
s->st.alloc_w_at = (void *) mmap_alloc_w_at;
|
|
s->st.sfree = (void *) mmap_sfree;
|
|
s->st.close = (void *) mmap_close;
|
|
s->st.seek = (void *) mmap_seek;
|
|
s->st.truncate = (void *) fd_truncate;
|
|
|
|
if (lseek (s->fd, s->file_length, SEEK_SET) < 0)
|
|
return FAILURE;
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
/*********************************************************************
|
|
memory stream functions - These are used for internal files
|
|
|
|
The idea here is that a single stream structure is created and all
|
|
requests must be satisfied from it. The location and size of the
|
|
buffer is the character variable supplied to the READ or WRITE
|
|
statement.
|
|
|
|
*********************************************************************/
|
|
|
|
|
|
static char *
|
|
mem_alloc_r_at (unix_stream * s, int *len, gfc_offset where)
|
|
{
|
|
gfc_offset n;
|
|
|
|
if (where == -1)
|
|
where = s->logical_offset;
|
|
|
|
if (where < s->buffer_offset || where > s->buffer_offset + s->active)
|
|
return NULL;
|
|
|
|
s->logical_offset = where + *len;
|
|
|
|
n = s->buffer_offset + s->active - where;
|
|
if (*len > n)
|
|
*len = n;
|
|
|
|
return s->buffer + (where - s->buffer_offset);
|
|
}
|
|
|
|
|
|
static char *
|
|
mem_alloc_w_at (unix_stream * s, int *len, gfc_offset where)
|
|
{
|
|
gfc_offset m;
|
|
|
|
if (where == -1)
|
|
where = s->logical_offset;
|
|
|
|
m = where + *len;
|
|
|
|
if (where < s->buffer_offset || m > s->buffer_offset + s->active)
|
|
return NULL;
|
|
|
|
s->logical_offset = m;
|
|
|
|
return s->buffer + (where - s->buffer_offset);
|
|
}
|
|
|
|
|
|
static int
|
|
mem_seek (unix_stream * s, gfc_offset offset)
|
|
{
|
|
if (offset > s->file_length)
|
|
{
|
|
errno = ESPIPE;
|
|
return FAILURE;
|
|
}
|
|
|
|
s->logical_offset = offset;
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
static int
|
|
mem_truncate (unix_stream * s)
|
|
{
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
static try
|
|
mem_close (unix_stream * s)
|
|
{
|
|
free_mem (s);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
static try
|
|
mem_sfree (unix_stream * s)
|
|
{
|
|
return SUCCESS;
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************
|
|
Public functions -- A reimplementation of this module needs to
|
|
define functional equivalents of the following.
|
|
*********************************************************************/
|
|
|
|
/* empty_internal_buffer()-- Zero the buffer of Internal file */
|
|
|
|
void
|
|
empty_internal_buffer(stream *strm)
|
|
{
|
|
unix_stream * s = (unix_stream *) strm;
|
|
memset(s->buffer, ' ', s->file_length);
|
|
}
|
|
|
|
/* open_internal()-- Returns a stream structure from an internal file */
|
|
|
|
stream *
|
|
open_internal (char *base, int length)
|
|
{
|
|
unix_stream *s;
|
|
|
|
s = get_mem (sizeof (unix_stream));
|
|
memset (s, '\0', sizeof (unix_stream));
|
|
|
|
s->buffer = base;
|
|
s->buffer_offset = 0;
|
|
|
|
s->logical_offset = 0;
|
|
s->active = s->file_length = length;
|
|
|
|
s->st.alloc_r_at = (void *) mem_alloc_r_at;
|
|
s->st.alloc_w_at = (void *) mem_alloc_w_at;
|
|
s->st.sfree = (void *) mem_sfree;
|
|
s->st.close = (void *) mem_close;
|
|
s->st.seek = (void *) mem_seek;
|
|
s->st.truncate = (void *) mem_truncate;
|
|
|
|
return (stream *) s;
|
|
}
|
|
|
|
|
|
/* fd_to_stream()-- Given an open file descriptor, build a stream
|
|
* around it. */
|
|
|
|
static stream *
|
|
fd_to_stream (int fd, int prot, int avoid_mmap)
|
|
{
|
|
struct stat statbuf;
|
|
unix_stream *s;
|
|
|
|
s = get_mem (sizeof (unix_stream));
|
|
memset (s, '\0', sizeof (unix_stream));
|
|
|
|
s->fd = fd;
|
|
s->buffer_offset = 0;
|
|
s->physical_offset = 0;
|
|
s->logical_offset = 0;
|
|
s->prot = prot;
|
|
|
|
/* Get the current length of the file. */
|
|
|
|
fstat (fd, &statbuf);
|
|
s->file_length = S_ISREG (statbuf.st_mode) ? statbuf.st_size : -1;
|
|
|
|
#if HAVE_MMAP
|
|
if (avoid_mmap)
|
|
fd_open (s);
|
|
else
|
|
mmap_open (s);
|
|
#else
|
|
fd_open (s);
|
|
#endif
|
|
|
|
return (stream *) s;
|
|
}
|
|
|
|
|
|
/* Given the Fortran unit number, convert it to a C file descriptor. */
|
|
|
|
int
|
|
unit_to_fd(int unit)
|
|
{
|
|
gfc_unit *us;
|
|
|
|
us = find_unit(unit);
|
|
if (us == NULL)
|
|
return -1;
|
|
|
|
return ((unix_stream *) us->s)->fd;
|
|
}
|
|
|
|
|
|
/* unpack_filename()-- Given a fortran string and a pointer to a
|
|
* buffer that is PATH_MAX characters, convert the fortran string to a
|
|
* C string in the buffer. Returns nonzero if this is not possible. */
|
|
|
|
static int
|
|
unpack_filename (char *cstring, const char *fstring, int len)
|
|
{
|
|
len = fstrlen (fstring, len);
|
|
if (len >= PATH_MAX)
|
|
return 1;
|
|
|
|
memmove (cstring, fstring, len);
|
|
cstring[len] = '\0';
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* tempfile()-- Generate a temporary filename for a scratch file and
|
|
* open it. mkstemp() opens the file for reading and writing, but the
|
|
* library mode prevents anything that is not allowed. The descriptor
|
|
* is returned, which is -1 on error. The template is pointed to by
|
|
* ioparm.file, which is copied into the unit structure
|
|
* and freed later. */
|
|
|
|
static int
|
|
tempfile (void)
|
|
{
|
|
const char *tempdir;
|
|
char *template;
|
|
int fd;
|
|
|
|
tempdir = getenv ("GFORTRAN_TMPDIR");
|
|
if (tempdir == NULL)
|
|
tempdir = getenv ("TMP");
|
|
if (tempdir == NULL)
|
|
tempdir = DEFAULT_TEMPDIR;
|
|
|
|
template = get_mem (strlen (tempdir) + 20);
|
|
|
|
st_sprintf (template, "%s/gfortrantmpXXXXXX", tempdir);
|
|
|
|
#ifdef HAVE_MKSTEMP
|
|
|
|
fd = mkstemp (template);
|
|
|
|
#else /* HAVE_MKSTEMP */
|
|
|
|
if (mktemp (template))
|
|
do
|
|
fd = open (template, O_CREAT | O_EXCL, S_IREAD | S_IWRITE);
|
|
while (!(fd == -1 && errno == EEXIST) && mktemp (template));
|
|
else
|
|
fd = -1;
|
|
|
|
#endif /* HAVE_MKSTEMP */
|
|
|
|
if (fd < 0)
|
|
free_mem (template);
|
|
else
|
|
{
|
|
ioparm.file = template;
|
|
ioparm.file_len = strlen (template); /* Don't include trailing nul */
|
|
}
|
|
|
|
return fd;
|
|
}
|
|
|
|
|
|
/* regular_file()-- Open a regular file.
|
|
* Change flags->action if it is ACTION_UNSPECIFIED on entry,
|
|
* unless an error occurs.
|
|
* Returns the descriptor, which is less than zero on error. */
|
|
|
|
static int
|
|
regular_file (unit_flags *flags)
|
|
{
|
|
char path[PATH_MAX + 1];
|
|
int mode;
|
|
int rwflag;
|
|
int crflag;
|
|
int fd;
|
|
|
|
if (unpack_filename (path, ioparm.file, ioparm.file_len))
|
|
{
|
|
errno = ENOENT; /* Fake an OS error */
|
|
return -1;
|
|
}
|
|
|
|
rwflag = 0;
|
|
|
|
switch (flags->action)
|
|
{
|
|
case ACTION_READ:
|
|
rwflag = O_RDONLY;
|
|
break;
|
|
|
|
case ACTION_WRITE:
|
|
rwflag = O_WRONLY;
|
|
break;
|
|
|
|
case ACTION_READWRITE:
|
|
case ACTION_UNSPECIFIED:
|
|
rwflag = O_RDWR;
|
|
break;
|
|
|
|
default:
|
|
internal_error ("regular_file(): Bad action");
|
|
}
|
|
|
|
switch (flags->status)
|
|
{
|
|
case STATUS_NEW:
|
|
crflag = O_CREAT | O_EXCL;
|
|
break;
|
|
|
|
case STATUS_OLD: /* open will fail if the file does not exist*/
|
|
crflag = 0;
|
|
break;
|
|
|
|
case STATUS_UNKNOWN:
|
|
case STATUS_SCRATCH:
|
|
crflag = O_CREAT;
|
|
break;
|
|
|
|
case STATUS_REPLACE:
|
|
crflag = O_CREAT | O_TRUNC;
|
|
break;
|
|
|
|
default:
|
|
internal_error ("regular_file(): Bad status");
|
|
}
|
|
|
|
/* rwflag |= O_LARGEFILE; */
|
|
|
|
mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
|
|
fd = open (path, rwflag | crflag, mode);
|
|
if (flags->action != ACTION_UNSPECIFIED)
|
|
return fd;
|
|
|
|
if (fd >= 0)
|
|
{
|
|
flags->action = ACTION_READWRITE;
|
|
return fd;
|
|
}
|
|
if (errno != EACCES)
|
|
return fd;
|
|
|
|
/* retry for read-only access */
|
|
rwflag = O_RDONLY;
|
|
fd = open (path, rwflag | crflag, mode);
|
|
if (fd >=0)
|
|
{
|
|
flags->action = ACTION_READ;
|
|
return fd; /* success */
|
|
}
|
|
|
|
if (errno != EACCES)
|
|
return fd; /* failure */
|
|
|
|
/* retry for write-only access */
|
|
rwflag = O_WRONLY;
|
|
fd = open (path, rwflag | crflag, mode);
|
|
if (fd >=0)
|
|
{
|
|
flags->action = ACTION_WRITE;
|
|
return fd; /* success */
|
|
}
|
|
return fd; /* failure */
|
|
}
|
|
|
|
|
|
/* open_external()-- Open an external file, unix specific version.
|
|
* Change flags->action if it is ACTION_UNSPECIFIED on entry.
|
|
* Returns NULL on operating system error. */
|
|
|
|
stream *
|
|
open_external (unit_flags *flags)
|
|
{
|
|
int fd, prot;
|
|
|
|
if (flags->status == STATUS_SCRATCH)
|
|
{
|
|
fd = tempfile ();
|
|
if (flags->action == ACTION_UNSPECIFIED)
|
|
flags->action = ACTION_READWRITE;
|
|
/* We can unlink scratch files now and it will go away when closed. */
|
|
unlink (ioparm.file);
|
|
}
|
|
else
|
|
{
|
|
/* regular_file resets flags->action if it is ACTION_UNSPECIFIED and
|
|
* if it succeeds */
|
|
fd = regular_file (flags);
|
|
}
|
|
|
|
if (fd < 0)
|
|
return NULL;
|
|
fd = fix_fd (fd);
|
|
|
|
switch (flags->action)
|
|
{
|
|
case ACTION_READ:
|
|
prot = PROT_READ;
|
|
break;
|
|
|
|
case ACTION_WRITE:
|
|
prot = PROT_WRITE;
|
|
break;
|
|
|
|
case ACTION_READWRITE:
|
|
prot = PROT_READ | PROT_WRITE;
|
|
break;
|
|
|
|
default:
|
|
internal_error ("open_external(): Bad action");
|
|
}
|
|
|
|
return fd_to_stream (fd, prot, 0);
|
|
}
|
|
|
|
|
|
/* input_stream()-- Return a stream pointer to the default input stream.
|
|
* Called on initialization. */
|
|
|
|
stream *
|
|
input_stream (void)
|
|
{
|
|
return fd_to_stream (STDIN_FILENO, PROT_READ, 1);
|
|
}
|
|
|
|
|
|
/* output_stream()-- Return a stream pointer to the default output stream.
|
|
* Called on initialization. */
|
|
|
|
stream *
|
|
output_stream (void)
|
|
{
|
|
return fd_to_stream (STDOUT_FILENO, PROT_WRITE, 1);
|
|
}
|
|
|
|
|
|
/* error_stream()-- Return a stream pointer to the default error stream.
|
|
* Called on initialization. */
|
|
|
|
stream *
|
|
error_stream (void)
|
|
{
|
|
return fd_to_stream (STDERR_FILENO, PROT_WRITE, 1);
|
|
}
|
|
|
|
/* init_error_stream()-- Return a pointer to the error stream. This
|
|
* subroutine is called when the stream is needed, rather than at
|
|
* initialization. We want to work even if memory has been seriously
|
|
* corrupted. */
|
|
|
|
stream *
|
|
init_error_stream (void)
|
|
{
|
|
static unix_stream error;
|
|
|
|
memset (&error, '\0', sizeof (error));
|
|
|
|
error.fd = options.use_stderr ? STDERR_FILENO : STDOUT_FILENO;
|
|
|
|
error.st.alloc_w_at = (void *) fd_alloc_w_at;
|
|
error.st.sfree = (void *) fd_sfree;
|
|
|
|
error.unbuffered = 1;
|
|
error.buffer = error.small_buffer;
|
|
|
|
return (stream *) & error;
|
|
}
|
|
|
|
|
|
/* compare_file_filename()-- Given an open stream and a fortran string
|
|
* that is a filename, figure out if the file is the same as the
|
|
* filename. */
|
|
|
|
int
|
|
compare_file_filename (stream * s, const char *name, int len)
|
|
{
|
|
char path[PATH_MAX + 1];
|
|
struct stat st1, st2;
|
|
|
|
if (unpack_filename (path, name, len))
|
|
return 0; /* Can't be the same */
|
|
|
|
/* If the filename doesn't exist, then there is no match with the
|
|
* existing file. */
|
|
|
|
if (stat (path, &st1) < 0)
|
|
return 0;
|
|
|
|
fstat (((unix_stream *) s)->fd, &st2);
|
|
|
|
return (st1.st_dev == st2.st_dev) && (st1.st_ino == st2.st_ino);
|
|
}
|
|
|
|
|
|
/* find_file0()-- Recursive work function for find_file() */
|
|
|
|
static gfc_unit *
|
|
find_file0 (gfc_unit * u, struct stat *st1)
|
|
{
|
|
struct stat st2;
|
|
gfc_unit *v;
|
|
|
|
if (u == NULL)
|
|
return NULL;
|
|
|
|
if (fstat (((unix_stream *) u->s)->fd, &st2) >= 0 &&
|
|
st1->st_dev == st2.st_dev && st1->st_ino == st2.st_ino)
|
|
return u;
|
|
|
|
v = find_file0 (u->left, st1);
|
|
if (v != NULL)
|
|
return v;
|
|
|
|
v = find_file0 (u->right, st1);
|
|
if (v != NULL)
|
|
return v;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/* find_file()-- Take the current filename and see if there is a unit
|
|
* that has the file already open. Returns a pointer to the unit if so. */
|
|
|
|
gfc_unit *
|
|
find_file (void)
|
|
{
|
|
char path[PATH_MAX + 1];
|
|
struct stat statbuf;
|
|
|
|
if (unpack_filename (path, ioparm.file, ioparm.file_len))
|
|
return NULL;
|
|
|
|
if (stat (path, &statbuf) < 0)
|
|
return NULL;
|
|
|
|
return find_file0 (g.unit_root, &statbuf);
|
|
}
|
|
|
|
|
|
/* stream_at_bof()-- Returns nonzero if the stream is at the beginning
|
|
* of the file. */
|
|
|
|
int
|
|
stream_at_bof (stream * s)
|
|
{
|
|
unix_stream *us;
|
|
|
|
us = (unix_stream *) s;
|
|
|
|
if (!us->mmaped)
|
|
return 0; /* File is not seekable */
|
|
|
|
return us->logical_offset == 0;
|
|
}
|
|
|
|
|
|
/* stream_at_eof()-- Returns nonzero if the stream is at the beginning
|
|
* of the file. */
|
|
|
|
int
|
|
stream_at_eof (stream * s)
|
|
{
|
|
unix_stream *us;
|
|
|
|
us = (unix_stream *) s;
|
|
|
|
if (!us->mmaped)
|
|
return 0; /* File is not seekable */
|
|
|
|
return us->logical_offset == us->dirty_offset;
|
|
}
|
|
|
|
|
|
/* delete_file()-- Given a unit structure, delete the file associated
|
|
* with the unit. Returns nonzero if something went wrong. */
|
|
|
|
int
|
|
delete_file (gfc_unit * u)
|
|
{
|
|
char path[PATH_MAX + 1];
|
|
|
|
if (unpack_filename (path, u->file, u->file_len))
|
|
{ /* Shouldn't be possible */
|
|
errno = ENOENT;
|
|
return 1;
|
|
}
|
|
|
|
return unlink (path);
|
|
}
|
|
|
|
|
|
/* file_exists()-- Returns nonzero if the current filename exists on
|
|
* the system */
|
|
|
|
int
|
|
file_exists (void)
|
|
{
|
|
char path[PATH_MAX + 1];
|
|
struct stat statbuf;
|
|
|
|
if (unpack_filename (path, ioparm.file, ioparm.file_len))
|
|
return 0;
|
|
|
|
if (stat (path, &statbuf) < 0)
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
|
|
static const char *yes = "YES", *no = "NO", *unknown = "UNKNOWN";
|
|
|
|
/* inquire_sequential()-- Given a fortran string, determine if the
|
|
* file is suitable for sequential access. Returns a C-style
|
|
* string. */
|
|
|
|
const char *
|
|
inquire_sequential (const char *string, int len)
|
|
{
|
|
char path[PATH_MAX + 1];
|
|
struct stat statbuf;
|
|
|
|
if (string == NULL ||
|
|
unpack_filename (path, string, len) || stat (path, &statbuf) < 0)
|
|
return unknown;
|
|
|
|
if (S_ISREG (statbuf.st_mode) ||
|
|
S_ISCHR (statbuf.st_mode) || S_ISFIFO (statbuf.st_mode))
|
|
return yes;
|
|
|
|
if (S_ISDIR (statbuf.st_mode) || S_ISBLK (statbuf.st_mode))
|
|
return no;
|
|
|
|
return unknown;
|
|
}
|
|
|
|
|
|
/* inquire_direct()-- Given a fortran string, determine if the file is
|
|
* suitable for direct access. Returns a C-style string. */
|
|
|
|
const char *
|
|
inquire_direct (const char *string, int len)
|
|
{
|
|
char path[PATH_MAX + 1];
|
|
struct stat statbuf;
|
|
|
|
if (string == NULL ||
|
|
unpack_filename (path, string, len) || stat (path, &statbuf) < 0)
|
|
return unknown;
|
|
|
|
if (S_ISREG (statbuf.st_mode) || S_ISBLK (statbuf.st_mode))
|
|
return yes;
|
|
|
|
if (S_ISDIR (statbuf.st_mode) ||
|
|
S_ISCHR (statbuf.st_mode) || S_ISFIFO (statbuf.st_mode))
|
|
return no;
|
|
|
|
return unknown;
|
|
}
|
|
|
|
|
|
/* inquire_formatted()-- Given a fortran string, determine if the file
|
|
* is suitable for formatted form. Returns a C-style string. */
|
|
|
|
const char *
|
|
inquire_formatted (const char *string, int len)
|
|
{
|
|
char path[PATH_MAX + 1];
|
|
struct stat statbuf;
|
|
|
|
if (string == NULL ||
|
|
unpack_filename (path, string, len) || stat (path, &statbuf) < 0)
|
|
return unknown;
|
|
|
|
if (S_ISREG (statbuf.st_mode) ||
|
|
S_ISBLK (statbuf.st_mode) ||
|
|
S_ISCHR (statbuf.st_mode) || S_ISFIFO (statbuf.st_mode))
|
|
return yes;
|
|
|
|
if (S_ISDIR (statbuf.st_mode))
|
|
return no;
|
|
|
|
return unknown;
|
|
}
|
|
|
|
|
|
/* inquire_unformatted()-- Given a fortran string, determine if the file
|
|
* is suitable for unformatted form. Returns a C-style string. */
|
|
|
|
const char *
|
|
inquire_unformatted (const char *string, int len)
|
|
{
|
|
return inquire_formatted (string, len);
|
|
}
|
|
|
|
|
|
/* inquire_access()-- Given a fortran string, determine if the file is
|
|
* suitable for access. */
|
|
|
|
static const char *
|
|
inquire_access (const char *string, int len, int mode)
|
|
{
|
|
char path[PATH_MAX + 1];
|
|
|
|
if (string == NULL || unpack_filename (path, string, len) ||
|
|
access (path, mode) < 0)
|
|
return no;
|
|
|
|
return yes;
|
|
}
|
|
|
|
|
|
/* inquire_read()-- Given a fortran string, determine if the file is
|
|
* suitable for READ access. */
|
|
|
|
const char *
|
|
inquire_read (const char *string, int len)
|
|
{
|
|
return inquire_access (string, len, R_OK);
|
|
}
|
|
|
|
|
|
/* inquire_write()-- Given a fortran string, determine if the file is
|
|
* suitable for READ access. */
|
|
|
|
const char *
|
|
inquire_write (const char *string, int len)
|
|
{
|
|
return inquire_access (string, len, W_OK);
|
|
}
|
|
|
|
|
|
/* inquire_readwrite()-- Given a fortran string, determine if the file is
|
|
* suitable for read and write access. */
|
|
|
|
const char *
|
|
inquire_readwrite (const char *string, int len)
|
|
{
|
|
return inquire_access (string, len, R_OK | W_OK);
|
|
}
|
|
|
|
|
|
/* file_length()-- Return the file length in bytes, -1 if unknown */
|
|
|
|
gfc_offset
|
|
file_length (stream * s)
|
|
{
|
|
return ((unix_stream *) s)->file_length;
|
|
}
|
|
|
|
|
|
/* file_position()-- Return the current position of the file */
|
|
|
|
gfc_offset
|
|
file_position (stream * s)
|
|
{
|
|
return ((unix_stream *) s)->logical_offset;
|
|
}
|
|
|
|
|
|
/* is_seekable()-- Return nonzero if the stream is seekable, zero if
|
|
* it is not */
|
|
|
|
int
|
|
is_seekable (stream * s)
|
|
{
|
|
/* by convention, if file_length == -1, the file is not seekable
|
|
note that a mmapped file is always seekable, an fd_ file may
|
|
or may not be. */
|
|
return ((unix_stream *) s)->file_length!=-1;
|
|
}
|
|
|
|
try
|
|
flush (stream *s)
|
|
{
|
|
return fd_flush( (unix_stream *) s);
|
|
}
|
|
|
|
|
|
/* How files are stored: This is an operating-system specific issue,
|
|
and therefore belongs here. There are three cases to consider.
|
|
|
|
Direct Access:
|
|
Records are written as block of bytes corresponding to the record
|
|
length of the file. This goes for both formatted and unformatted
|
|
records. Positioning is done explicitly for each data transfer,
|
|
so positioning is not much of an issue.
|
|
|
|
Sequential Formatted:
|
|
Records are separated by newline characters. The newline character
|
|
is prohibited from appearing in a string. If it does, this will be
|
|
messed up on the next read. End of file is also the end of a record.
|
|
|
|
Sequential Unformatted:
|
|
In this case, we are merely copying bytes to and from main storage,
|
|
yet we need to keep track of varying record lengths. We adopt
|
|
the solution used by f2c. Each record contains a pair of length
|
|
markers:
|
|
|
|
Length of record n in bytes
|
|
Data of record n
|
|
Length of record n in bytes
|
|
|
|
Length of record n+1 in bytes
|
|
Data of record n+1
|
|
Length of record n+1 in bytes
|
|
|
|
The length is stored at the end of a record to allow backspacing to the
|
|
previous record. Between data transfer statements, the file pointer
|
|
is left pointing to the first length of the current record.
|
|
|
|
ENDFILE records are never explicitly stored.
|
|
|
|
*/
|