(* Aos, Copyright 2001, Pieter Muller, ETH Zurich *)
MODULE Files IN Oberon; (* pjm *)
(** AUTHOR "pjm"; PURPOSE "Oberon for Aos files"; *)
IMPORT SYSTEM, KernelLog IN A2, AosKernel := Kernel IN A2, Files IN A2, Kernel;
CONST
BufSize = 4096;
MaxBufs = 4;
Slow = FALSE;
Trace = TRUE;
TYPE
File* = POINTER TO RECORD
buf: Buffer; (* circular list of buffers *)
bufs: LONGINT; (* number of buffers allocated *)
alen, blen: LONGINT; (* file size = alen*BufSize + blen, 0 <= blen <= BufSize *)
r: Files.Rider; (* rider on underlying Aos file *)
checktime, checkdate, checklen: LONGINT
END;
Rider* = RECORD
buf: Buffer; (* buffer hint *)
apos, bpos: LONGINT;
eof*: BOOLEAN; (** has end of file been passed *)
res*: LONGINT; (** leftover byte count for ReadBytes/WriteBytes *)
f: File
END;
Buffer = POINTER TO RECORD
apos, lim: LONGINT;
mod: BOOLEAN;
next: Buffer;
data: ARRAY BufSize OF CHAR
END;
Bytes4 = ARRAY 4 OF SYSTEM.BYTE;
Bytes8 = ARRAY 8 OF SYSTEM.BYTE;
VAR
files: AosKernel.FinalizedCollection; (* all open files - cleaned up by GC *)
search: Files.File; (* file being searched for *)
found: File; (* file found *)
(* Update our copy of the underlying file's time and length. *)
PROCEDURE UpdateFile(f: File);
BEGIN
f.r.file.GetDate(f.checktime, f.checkdate); f.checklen := f.r.file.Length()
END UpdateFile;
(* Check if our copy of the underlying file's time and length match the reality. *)
PROCEDURE FileChanged(f: File): BOOLEAN;
VAR time, date: LONGINT;
BEGIN
f.r.file.GetDate(time, date);
RETURN (time # f.checktime) OR (date # f.checkdate) OR (f.r.file.Length() # f.checklen)
END FileChanged;
(* Enumerator used in Old to search files collection for existing file handle using Files file as key. *)
PROCEDURE Search(f: ANY; VAR cont: BOOLEAN);
BEGIN
IF f(File).r.file = search THEN
found := f(File); cont := FALSE
END
END Search;
(** Creates a new file with the specified name. *)
PROCEDURE New*(CONST name: ARRAY OF CHAR): File;
VAR f: File; file: Files.File;
BEGIN
Kernel.CheckOberonLock; (* can only be called from Oberon *)
file := Files.New(name);
IF file # NIL THEN
NEW(f); f.bufs := 1; f.alen := 0; f.blen := 0;
NEW(f.buf); f.buf.apos := 0; f.buf.lim := 0; f.buf.next := f.buf; f.buf.mod := FALSE;
file.Set(f.r, 0); UpdateFile(f);
IF name # "" THEN
files.Add(f, NIL) (* add to collection *)
(* it is ok to add it here, and not only in Register, as in underlying file systems, because the underlying file system will take care of the case where an Old is attempted on a file that has been New'ed, but not Register'ed (Old will fail). *)
END
ELSE
f := NIL
END;
RETURN f
END New;
(** Open an existing file. The same file descriptor is returned if a file is opened multiple times. *)
PROCEDURE Old*(CONST name: ARRAY OF CHAR): File;
VAR f: File; file: Files.File; len: LONGINT;
BEGIN
Kernel.CheckOberonLock; (* can only be called from Oberon *)
file := Files.Old(name);
IF file # NIL THEN
search := file; found := NIL; (* search for existing handle *)
files.Enumerate(Search); (* modify global found *)
search := NIL; f := found; found := NIL;
IF (f # NIL) & FileChanged(f) THEN (* underlying file changed *)
IF Trace THEN
KernelLog.String("Files: Stale "); WriteFile(f); KernelLog.Ln
END;
files.Remove(f); f := NIL (* throw away old record (even though user may still have a copy; that is his fault) *)
END;
IF f = NIL THEN (* none found, create new handle *)
len := file.Length();
NEW(f); f.bufs := 1; f.alen := len DIV BufSize; f.blen := len MOD BufSize;
NEW(f.buf); f.buf.apos := 0; f.buf.next := f.buf; f.buf.mod := FALSE;
file.Set(f.r, 0); file.ReadBytes(f.r, f.buf.data, 0, BufSize);
IF f.alen = 0 THEN f.buf.lim := f.blen ELSE f.buf.lim := BufSize END;
UpdateFile(f);
files.Add(f, NIL) (* add to collection *)
ELSE
(* return existing handle *)
END
ELSE
f := NIL
END;
RETURN f
END Old;
(** Register a file created with New in the directory, replacing the previous file in the directory with the same name. The file is automatically closed. *)
PROCEDURE Register*(f: File);
BEGIN
Update(f); Files.Register(f.r.file)
END Register;
(** Flushes the changes made to a file to disk. Register will automatically Close a file. *)
PROCEDURE Close*(f: File);
BEGIN
IF f # NIL THEN Update(f) END
END Close;
(** Returns the current length of a file. *)
PROCEDURE Length*(f: File): LONGINT;
BEGIN
RETURN f.alen*BufSize + f.blen
END Length;
(** Returns the time (t) and date (d) when a file was last modified. *)
PROCEDURE GetDate*(f: File; VAR t, d: LONGINT);
BEGIN
f.r.file.GetDate(t, d)
END GetDate;
(** Sets the modification time (t) and date (d) of a file. *)
PROCEDURE SetDate*(f: File; t, d: LONGINT);
BEGIN
Update(f); (* otherwise later updating will modify time/date again *)
f.r.file.SetDate(t, d)
END SetDate;
(** Positions a Rider at a certain position in a file. Multiple Riders can be positioned at different locations in a file. A Rider cannot be positioned beyond the end of a file. *)
PROCEDURE Set*(VAR r: Rider; f: File; pos: LONGINT);
BEGIN
IF f # NIL THEN
r.eof := FALSE; r.res := 0; r.buf := f.buf; r.f := f;
IF pos < 0 THEN
r.apos := 0; r.bpos := 0
ELSIF pos < f.alen*BufSize + f.blen THEN
r.apos := pos DIV BufSize; r.bpos := pos MOD BufSize
ELSE
r.apos := f.alen; r.bpos := f.blen (* blen may be BufSize *)
END
ELSE
r.buf := NIL; r.f := NIL
END
END Set;
(** Returns the offset of a Rider positioned on a file. *)
PROCEDURE Pos*(VAR r: Rider): LONGINT;
BEGIN
RETURN r.apos*BufSize + r.bpos
END Pos;
(** Returns the File a Rider is based on. *)
PROCEDURE Base*(VAR r: Rider): File;
BEGIN
RETURN r.f
END Base;
(** Read a byte from a file, advancing the Rider one byte further. R.eof indicates if the end of the file has been passed. *)
PROCEDURE Read*(VAR r: Rider; VAR x: SYSTEM.BYTE);
VAR buf: Buffer;
BEGIN
buf := r.buf;
IF r.apos # buf.apos THEN buf := GetBuf(r.f, r.apos); r.buf := buf END;
IF r.bpos < buf.lim THEN
x := buf.data[r.bpos]; INC(r.bpos)
ELSIF r.apos < r.f.alen THEN
INC(r.apos);
buf := SearchBuf(r.f, r.apos);
IF buf = NIL THEN (* replace a buffer *)
buf := r.buf;
IF buf.mod THEN WriteBuf(r.f, buf) END;
ReadBuf(r.f, buf, r.apos)
ELSE
r.buf := buf
END;
IF buf.lim > 0 THEN
x := buf.data[0]; r.bpos := 1
ELSE
x := 0X; r.eof := TRUE
END
ELSE
x := 0X; r.eof := TRUE
END
END Read;
(** Reads a sequence of length n bytes into the buffer x, advancing the Rider. Less bytes will be read when reading over the length of the file. r.res indicates the number of unread bytes. x must be big enough to hold n bytes. *)
PROCEDURE ReadBytes*(VAR r: Rider; VAR x: ARRAY OF SYSTEM.BYTE; len: LONGINT);
VAR src, dst: ADDRESS; m: LONGINT; buf: Buffer; ch: CHAR;
BEGIN
IF LEN(x) < len THEN SYSTEM.HALT(19) END;
IF Slow THEN
m := 0;
LOOP
IF len <= 0 THEN EXIT END;
Read(r, ch);
IF r.eof THEN EXIT END;
x[m] := ch; INC(m); DEC(len)
END;
r.res := len
ELSE
IF len > 0 THEN
dst := ADDRESSOF(x[0]); buf := r.buf;
IF r.apos # buf.apos THEN buf := GetBuf(r.f, r.apos); r.buf := buf END;
LOOP
IF len <= 0 THEN EXIT END;
src := ADDRESSOF(buf.data[0]) + r.bpos; m := r.bpos + len;
IF m <= buf.lim THEN
SYSTEM.MOVE(src, dst, len); r.bpos := m; r.res := 0; EXIT
ELSIF buf.lim = BufSize THEN
m := buf.lim - r.bpos;
IF m > 0 THEN SYSTEM.MOVE(src, dst, m); INC(dst, m); DEC(len, m) END;
IF r.apos < r.f.alen THEN
INC(r.apos); r.bpos := 0; buf := SearchBuf(r.f, r.apos);
IF buf = NIL THEN
buf := r.buf;
IF buf.mod THEN WriteBuf(r.f, buf) END;
ReadBuf(r.f, buf, r.apos)
ELSE
r.buf := buf
END
ELSE
r.bpos := buf.lim; r.res := len; r.eof := TRUE; EXIT
END
ELSE
m := buf.lim - r.bpos;
IF m > 0 THEN SYSTEM.MOVE(src, dst, m); r.bpos := buf.lim END;
r.res := len - m; r.eof := TRUE; EXIT
END
END
ELSE
r.res := 0
END
END
END ReadBytes;
(**
Portable routines to read the standard Oberon types.
*)
PROCEDURE ReadInt*(VAR r: Rider; VAR x: INTEGER);
VAR x0, x1: SHORTINT;
BEGIN
Read(r, x0); Read(r, x1);
x := LONG(x1) * 100H + LONG(x0) MOD 100H
END ReadInt;
PROCEDURE ReadLInt*(VAR r: Rider; VAR x: LONGINT);
BEGIN
ReadBytes(r, SYSTEM.VAL(Bytes4, x), 4)
END ReadLInt;
PROCEDURE ReadSet*(VAR r: Rider; VAR x: SET);
BEGIN
ReadBytes(r, SYSTEM.VAL(Bytes4, x), 4)
END ReadSet;
PROCEDURE ReadBool*(VAR r: Rider; VAR x: BOOLEAN);
VAR s: SHORTINT;
BEGIN
Read(r, s); x := s # 0
END ReadBool;
PROCEDURE ReadReal*(VAR r: Rider; VAR x: REAL);
BEGIN
ReadBytes(r, SYSTEM.VAL(Bytes4, x), 4)
END ReadReal;
PROCEDURE ReadLReal*(VAR r: Rider; VAR x: LONGREAL);
BEGIN
ReadBytes(r, SYSTEM.VAL(Bytes8, x), 8)
END ReadLReal;
PROCEDURE ReadString*(VAR r: Rider; VAR x: ARRAY OF CHAR);
VAR i: INTEGER; ch: CHAR;
BEGIN i := 0;
LOOP
Read(r, ch); x[i] := ch; INC(i);
IF ch = 0X THEN EXIT END;
IF i = LEN(x) THEN x[i-1] := 0X;
REPEAT Read(r, ch) UNTIL ch = 0X;
EXIT
END
END
END ReadString;
(** Reads a number in compressed variable length notation using the minimum amount of bytes. *)
PROCEDURE ReadNum*(VAR r: Rider; VAR x: LONGINT);
VAR ch: CHAR; n: INTEGER; y: LONGINT;
BEGIN
n := 0; y := 0; Read(r, ch);
WHILE ch >= 80X DO INC(y, LSH(LONG(ORD(ch)) - 128, n)); INC(n, 7); Read(r, ch) END;
x := ASH(LSH(LONG(ORD(ch)), 25), n-25) + y
END ReadNum;
(** Writes a byte into the file at the Rider position, advancing the Rider by one. *)
PROCEDURE Write*(VAR r: Rider; x: SYSTEM.BYTE);
VAR buf: Buffer;
BEGIN
buf := r.buf;
IF r.apos # buf.apos THEN buf := GetBuf(r.f, r.apos); r.buf := buf END;
IF r.bpos >= buf.lim THEN
IF r.bpos < BufSize THEN
INC(buf.lim); INC(r.f.blen) (* blen may become BufSize *)
ELSE
buf.lim := BufSize; (* used by WriteBuf *)
WriteBuf(r.f, buf); INC(r.apos); buf := SearchBuf(r.f, r.apos);
IF buf = NIL THEN
buf := r.buf;
IF r.apos <= r.f.alen THEN
ReadBuf(r.f, buf, r.apos)
ELSE
buf.apos := r.apos; buf.lim := 1; INC(r.f.alen); r.f.blen := 1
END
ELSE
r.buf := buf
END;
r.bpos := 0
END
END;
buf.data[r.bpos] := CHR(x); INC(r.bpos); buf.mod := TRUE
END Write;
(** Writes the buffer x containing n bytes into a file at the Rider position. *)
PROCEDURE WriteBytes*(VAR r: Rider; CONST x: ARRAY OF SYSTEM.BYTE; len: LONGINT);
VAR src, dst: ADDRESS; m: LONGINT; buf: Buffer;
BEGIN
IF LEN(x) < len THEN SYSTEM.HALT(19) END;
IF Slow THEN
m := 0;
WHILE len > 0 DO
Write(r, x[m]); INC(m); DEC(len)
END;
r.res := len
ELSE
IF len > 0 THEN
src := ADDRESSOF(x[0]);
buf := r.buf;
IF r.apos # buf.apos THEN buf := GetBuf(r.f, r.apos); r.buf := buf END;
LOOP
IF len <= 0 THEN EXIT END;
buf.mod := TRUE; dst := ADDRESSOF(buf.data[0]) + r.bpos; m := r.bpos + len;
IF m <= buf.lim THEN
SYSTEM.MOVE(src, dst, len); r.bpos := m; EXIT
ELSIF m <= BufSize THEN
SYSTEM.MOVE(src, dst, len); r.bpos := m;
r.f.blen := m; buf.lim := m; EXIT
ELSE
buf.lim := BufSize; (* used by WriteBuf *)
m := BufSize - r.bpos;
IF m > 0 THEN SYSTEM.MOVE(src, dst, m); INC(src, m); DEC(len, m) END;
WriteBuf(r.f, buf); INC(r.apos); r.bpos := 0; buf := SearchBuf(r.f, r.apos);
IF buf = NIL THEN
buf := r.buf;
IF r.apos <= r.f.alen THEN
ReadBuf(r.f, buf, r.apos)
ELSE
buf.apos := r.apos; buf.lim := 0; INC(r.f.alen); r.f.blen := 0
END
ELSE
r.buf := buf
END
END
END
END
END
END WriteBytes;
(**
Portable routines to write the standard Oberon types.
*)
PROCEDURE WriteInt*(VAR r: Rider; x: INTEGER);
BEGIN
Write(r, SHORT(x)); Write(r, SHORT(x DIV 100H))
END WriteInt;
PROCEDURE WriteLInt*(VAR r: Rider; x: LONGINT);
BEGIN
WriteBytes(r, SYSTEM.VAL(Bytes4, x), 4)
END WriteLInt;
PROCEDURE WriteSet*(VAR r: Rider; x: SET);
BEGIN
WriteBytes(r, SYSTEM.VAL(Bytes4, x), 4)
END WriteSet;
PROCEDURE WriteBool*(VAR r: Rider; x: BOOLEAN);
BEGIN
IF x THEN Write(r, 1) ELSE Write(r, 0) END
END WriteBool;
PROCEDURE WriteReal*(VAR r: Rider; x: REAL);
BEGIN
WriteBytes(r, SYSTEM.VAL(Bytes4, x), 4)
END WriteReal;
PROCEDURE WriteLReal*(VAR r: Rider; x: LONGREAL);
BEGIN
WriteBytes(r, SYSTEM.VAL(Bytes8, x), 8)
END WriteLReal;
PROCEDURE WriteString*(VAR r: Rider; CONST x: ARRAY OF CHAR);
VAR i: INTEGER; ch: CHAR;
BEGIN
i := 0;
LOOP ch := x[i]; Write(r, ch); INC(i);
IF ch = 0X THEN EXIT END;
IF i = LEN(x) THEN Write(r, 0X); EXIT END
END
END WriteString;
(** Writes a number in a compressed format. *)
PROCEDURE WriteNum*(VAR r: Rider; x: LONGINT);
BEGIN
WHILE (x < - 64) OR (x > 63) DO Write(r, CHR(x MOD 128 + 128)); x := x DIV 128 END;
Write(r, CHR(x MOD 128))
END WriteNum;
(** Deletes a file. res = 0 indicates success. *)
PROCEDURE Delete*(name: ARRAY OF CHAR; VAR res: INTEGER);
VAR r: LONGINT;
BEGIN
Files.Delete(name, r);
IF (r >= MIN(INTEGER)) & (r <= MAX(INTEGER)) THEN res := SHORT(r) ELSE res := -1 END
END Delete;
(** Renames a file. res = 0 indicates success. *)
PROCEDURE Rename*(CONST old, new: ARRAY OF CHAR; VAR res: INTEGER);
VAR r: LONGINT;
BEGIN
Files.Rename(old, new, r);
IF (r >= MIN(INTEGER)) & (r <= MAX(INTEGER)) THEN res := SHORT(r) ELSE res := -1 END
END Rename;
(** Returns the full name of a file. *)
PROCEDURE GetName*(f: File; VAR name: ARRAY OF CHAR);
BEGIN
f.r.file.GetName(name)
END GetName;
PROCEDURE ReadBuf(f: File; buf: Buffer; pos: LONGINT);
VAR file: Files.File;
BEGIN
file := f.r.file;
file.Set(f.r, pos*BufSize);
ASSERT(file.Pos(f.r) = pos*BufSize);
file.ReadBytes(f.r, buf.data, 0, BufSize);
IF pos < f.alen THEN buf.lim := BufSize ELSE buf.lim := f.blen END;
buf.apos := pos; buf.mod := FALSE;
END ReadBuf;
PROCEDURE WriteBuf(f: File; buf: Buffer);
VAR pos, n: LONGINT; file: Files.File;
BEGIN
file := f.r.file;
pos := buf.apos*BufSize;
n := pos - file.Length();
IF n > 0 THEN (* pos is past current eof, extend file *)
file.Set(f.r, file.Length());
WHILE n > 0 DO file.Write(f.r, 0X); DEC(n) END
END;
file.Set(f.r, pos);
ASSERT(file.Pos(f.r) = pos);
file.WriteBytes(f.r, buf.data, 0, buf.lim);
UpdateFile(f);
buf.mod := FALSE
END WriteBuf;
PROCEDURE SearchBuf(f: File; pos: LONGINT): Buffer;
VAR buf: Buffer;
BEGIN
buf := f.buf;
LOOP
IF buf.apos = pos THEN EXIT END;
buf := buf.next;
IF buf = f.buf THEN buf := NIL; EXIT END
END;
RETURN buf
END SearchBuf;
PROCEDURE GetBuf(f: File; pos: LONGINT): Buffer;
VAR buf: Buffer;
BEGIN
buf := f.buf;
LOOP
IF buf.apos = pos THEN EXIT END;
IF buf.next = f.buf THEN
IF f.bufs < MaxBufs THEN
NEW(buf); buf.next := f.buf.next; f.buf.next := buf;
INC(f.bufs)
ELSE
f.buf := buf;
IF buf.mod THEN WriteBuf(f, buf) END
END;
buf.apos := pos;
IF pos <= f.alen THEN ReadBuf(f, buf, pos) END; (* ELSE? *)
EXIT
END;
buf := buf.next
END;
RETURN buf
END GetBuf;
PROCEDURE Update(f: File);
VAR buf: Buffer;
BEGIN
buf := f.buf;
REPEAT
IF buf.mod THEN WriteBuf(f, buf) END;
buf := buf.next
UNTIL buf = f.buf;
f.r.file.Update(); (* update the underlying file also *)
UpdateFile(f)
END Update;
PROCEDURE WriteFile(f: File);
VAR name: ARRAY 64 OF CHAR;
BEGIN
IF Trace THEN
KernelLog.Hex(SYSTEM.VAL(LONGINT, f), 8); KernelLog.Char(" ");
KernelLog.Hex(SYSTEM.VAL(LONGINT, f.r.file), 1); KernelLog.Char(" ");
KernelLog.Int(Length(f), 1); KernelLog.Char(" ");
KernelLog.Int(f.r.file.Length(), 1); KernelLog.Char(" ");
GetName(f, name);
KernelLog.String(name)
END
END WriteFile;
(* debugging *)
(*
PROCEDURE ShowList*;
VAR
enum: OBJECT
VAR i: LONGINT;
PROCEDURE EnumFile(f: ANY; VAR cont: BOOLEAN);
BEGIN
WITH f: File DO
KernelLog.Int(i, 1); KernelLog.Char(" ");
WriteFile(f); KernelLog.Ln;
INC(i)
END
END EnumFile;
END;
BEGIN
NEW(enum); enum.i := 0; KernelLog.Ln;
files.Enumerate(enum.EnumFile)
END ShowList;
*)
BEGIN
NEW(files)
END Files.
(** Remarks:
1. Oberon uses the little-endian byte ordering for exchanging files between different Oberon platforms.
2. Files are separate entities from directory entries. Files may be anonymous by having no name and not being registered in a directory. Files only become visible to other clients of the Files module by explicitly passing a File descriptor or by registering a file and then opening it from the other client. Deleting a file of which a file descriptor is still available, results in the file becoming anonymous. The deleted file may be re-registered at any time.
3. Files and their access mechanism (Riders) are separated. A file might have more than one rider operating on it at different offsets in the file.
4. The garbage collector will automatically close files when they are not required any more. File buffers will be discarded without flushing them to disk. Use the Close procedure to update modified files on disk.
5. Relative and absolute filenames written in the directory syntax of the host operating system are used. By convention, Oberon filenames consists of the letters A..Z, a..z, 0..9, and ".". The directory separator is typically / or :. Oberon filenames are case sensitive. *)
(*
to do:
o Rename duplicate methods/procedures in Files (e.g. Register0 method)
o remove Read/Write methods to encourage buffering (bad idea?)
- handle case where underlying file is changed by someone else (e.g. a log file being written by an active object)
- check if file handle is a good "key" (yes, because it can not be re-used while we hold it in the list, through the rider)
*)