(* Aos, Copyright 2001, Pieter Muller, ETH Zurich *)

MODULE ISO9660Files;	(** AUTHOR "?/be"; PURPOSE "ISO 9660 File System (ported from Native Oberon)"; *)

	IMPORT SYSTEM, Modules, Files, KernelLog, Strings;

	CONST
		debug = FALSE; nameDebug = FALSE;

		SS = 2048;
		MaxBufs = 4;

		Directory = 1;

		eFileDoesNotExist = 8903;
		eCannotOpenSubDir = 8916;
		eInvalidFirstCluster = 8917;
		eNameIsWild = 8927;
		eInvalidFileName = 8941;
		eInvalidVolume = 9000;

	TYPE
		Filename = ARRAY 256 OF CHAR;

		VolDesc = POINTER TO RECORD
			root, rootDirSize: LONGINT (* sector number of root directory and root directory size *)
		END;

		Buffer = POINTER TO RECORD (Files.Hint)
			pos, lim: LONGINT;
			next: Buffer;
			data: POINTER TO ARRAY OF CHAR
		END;

		FileSystem = OBJECT(Files.FileSystem)
			VAR
				pri, sup, cur: VolDesc;
				jolietLevel: LONGINT;

			(** Open an existing file. The same file descriptor is returned if a file is opened multiple times.  End users use Files.Old instead. *)
			PROCEDURE Old0(name: ARRAY OF CHAR): Files.File;
			VAR f: File; namebuf: Filename; dircl, dirpos, time, date, filecl, len: LONGINT; attr: SET; res: INTEGER;
			BEGIN {EXCLUSIVE}
				res := 0; f := NIL;
				Check(name, namebuf, res);
				IF res = 0 THEN
					LocateFile(namebuf, SELF, dircl, dirpos, time, date, filecl, len, attr, res);
					IF debug THEN LogString("Old0; filecl: "); LogInt(filecl); LogLn END;
					IF res = 0 THEN
						IF Directory IN attr THEN res := eCannotOpenSubDir
						ELSIF filecl < 16 THEN res := eInvalidFirstCluster
						ELSE f := OpenFile(namebuf, SELF, dircl, dirpos, time, date, filecl, len, attr)
						END
					END
				END;
				RETURN f
			END Old0;

			(** Enumerate canonical file names. mask may contain * wildcards.  For internal use only.  End users use Enumerator instead. *)
			PROCEDURE Enumerate0(mask: ARRAY OF CHAR; flags: SET; enum: Files.Enumerator);
			VAR
				fname, name, mmask, pname, fullname: Filename;
				f: Files.File; R: Files.Rider;
				attr: SET; bAddToEnum: BOOLEAN;
				pos, time, date, len, cl: LONGINT; res: INTEGER;
			BEGIN {EXCLUSIVE}
				Check(mask, name, res);
				IF (res = 0) OR (res = eNameIsWild) THEN
					SeparateName(name, name, mmask); IF (mmask = "") THEN COPY("*", mmask) END;
					Files.JoinName(prefix, name, pname);
					IF nameDebug THEN LogString("Enumerate; dir name: "); LogString(pname); LogLn END;
					f := OldDir(SELF, name);
					IF f # NIL THEN
						f.Set(R, 0); pos := 0;
						LOOP
							MatchFile(R, mmask, fname, pos, cl, time, date, len, attr, res);
							IF res # 0 THEN EXIT END;
							COPY(pname, fullname);
							IF name[0] # 0X THEN Files.AppendStr("/", fullname) END;
							Files.AppendStr(fname, fullname);

							bAddToEnum := TRUE;
							IF Directory IN attr THEN
								IF (fname = ".") OR (fname = "..") THEN
									bAddToEnum := FALSE;
								END;
								flags := { Files.Directory };
								len := 0;
							ELSE
								flags := {};
							END;

							IF bAddToEnum THEN
								enum.PutEntry(fullname, flags, time, date, len)
							END;
						END;
					ELSE res := eFileDoesNotExist
					END
				END
			END Enumerate0;

			(** Return the unique non-zero key of the named file, if it exists. *)
			PROCEDURE FileKey*(name: ARRAY OF CHAR): LONGINT;
			VAR res: INTEGER; namebuf: Filename; t, key, filecl: LONGINT; attr: SET;
			BEGIN {EXCLUSIVE}
				IF nameDebug THEN LogString("OFSFATFiles.FileKey: "); LogString(name); LogLn END;
				key := 0;
				Check(name, namebuf, res);
				IF res = 0 THEN
					LocateFile(namebuf, SELF, t, t, t, t, filecl, t, attr, res);
					IF res = 0 THEN key := filecl END
				END;
				RETURN key
			END FileKey;

			(** Finalize the file system. *)
			PROCEDURE Finalize*;
			BEGIN {EXCLUSIVE}
				vol.Finalize;
				Finalize^
			END Finalize;
		END FileSystem;

		File = OBJECT (Files.File)
		VAR
			len,
			time, date,
			filecl: LONGINT;	(* first cluster *)
			attr: SET ;	(* ISO file attributes *)
			(* directory info *)
			name: Filename;
			dircl,	(* start cluster of dir. that contains entry for file *)
			dirpos: LONGINT;		(* position in cluster of dir. in which entry lies *)
			nofbufs: INTEGER;
			firstbuf: Buffer;

			PROCEDURE Set*(VAR r: Files.Rider; pos: LONGINT);
			BEGIN {EXCLUSIVE}
				r.eof := FALSE; r.res := 0; r.file := SELF; r.fs := fs;
				IF (pos < 0) THEN r.apos := 0
				ELSIF (pos < len) THEN r.apos := pos
				ELSE r.apos := len
				END;
				r.hint := firstbuf
			END Set;

			PROCEDURE Pos*(VAR r: Files.Rider): LONGINT;
			BEGIN {EXCLUSIVE}
				RETURN r.apos
			END Pos;

			PROCEDURE FindBuf(pos: LONGINT; hint: Buffer): Buffer;
			VAR buf: Buffer;
			BEGIN
				buf := hint;
				LOOP
					IF (pos >= buf.pos) & (pos < buf.pos+buf.lim) THEN EXIT END;
					buf := buf.next;
					IF buf = hint THEN buf := NIL;  EXIT END
				END;
				RETURN buf
			END FindBuf;

			PROCEDURE ReadBuf(buf: Buffer; pos: LONGINT);
			BEGIN
				ASSERT(pos <= len, 100);
				buf.pos := pos - pos MOD fs.vol.blockSize;
				pos := pos DIV fs.vol.blockSize;
				IF pos = len DIV fs.vol.blockSize THEN buf.lim := len MOD fs.vol.blockSize
				ELSE buf.lim := fs.vol.blockSize
				END;
				IF debug THEN LogString("ReadBuf; block: "); LogInt(filecl+pos); LogLn END;
				fs.vol.GetBlock(filecl+pos, buf.data^)
			END ReadBuf;

			PROCEDURE GetBuf(pos: LONGINT; hint: Buffer): Buffer;
			VAR buf: Buffer;
			BEGIN
				buf := FindBuf(pos, hint);
				IF buf = NIL THEN
					IF nofbufs < MaxBufs THEN (*allocate new buffer*)
						NEW(buf);  NEW(buf.data, fs.vol.blockSize);
						buf.next := firstbuf.next;  firstbuf.next := buf;
						INC(nofbufs)
					ELSE (*reuse one of the buffers; round robin *)
						buf := firstbuf; firstbuf := buf.next;
					END;
					ReadBuf(buf, pos);
				END;
				RETURN buf
			END GetBuf;

			PROCEDURE Read*(VAR r: Files.Rider; VAR x: CHAR);
			VAR buf: Buffer;
			BEGIN {EXCLUSIVE}
				r.res := 0;
				IF (r.apos < len) THEN
					buf := GetBuf(r.apos, r.hint(Buffer));
					x := buf.data[r.apos-buf.pos];
					INC(r.apos)
				ELSE
					x := 0X; r.eof := TRUE
				END
			END Read;

			PROCEDURE ReadBytes*(VAR r: Files.Rider; VAR x: ARRAY OF CHAR; ofs, len: LONGINT);
			VAR m, pos: LONGINT; src, dst: ADDRESS; buf: Buffer;
			BEGIN {EXCLUSIVE}
				IF LEN(x) < len THEN SYSTEM.HALT(19) END;
				IF len <= 0 THEN RETURN END;
				dst := ADDRESSOF(x[ofs]);
				buf := r.hint(Buffer);
				m := SELF.len - r.apos;
				IF len <= m THEN r.res := 0 ELSE r.eof := TRUE; r.res := len-m; len := m END;
				WHILE len > 0 DO
					buf := GetBuf(r.apos, buf);
					pos := r.apos - buf.pos;
					src := ADDRESSOF(buf.data[pos]);  m := buf.lim-pos;
					IF m > len THEN m := len END;
					SYSTEM.MOVE(src, dst, m);
					DEC(len, m); INC(dst, m); INC(r.apos, m);
				END;
				r.hint := buf
			END ReadBytes;

			PROCEDURE Length*(): LONGINT;
			BEGIN RETURN len
			END Length;

			PROCEDURE GetDate*(VAR t, d: LONGINT);
			BEGIN t := time; d := date
			END GetDate;

			PROCEDURE GetName*(VAR name: ARRAY OF CHAR);
			BEGIN COPY(SELF.name, name)
			END GetName;

			PROCEDURE Update*;
			END Update; (* nothing *)
		END File;

	VAR	(* svr *)
		ExtractNameProc: PROCEDURE(VAR dir, name: ARRAY OF CHAR);

	(* debug procedures *)

	PROCEDURE LogString(s: ARRAY OF CHAR);
	BEGIN
		KernelLog.String(s)
	END LogString;

	PROCEDURE LogInt(i: LONGINT);
	BEGIN
		KernelLog.Int(i, 0)
	END LogInt;

	PROCEDURE LogLn;
	BEGIN
		KernelLog.Ln
	END LogLn;

	(* help procedures *)

	(* Get733 - 32 bit, both byte orders *)
	PROCEDURE Get733(VAR s: ARRAY OF CHAR; first: LONGINT; VAR d: LONGINT);
	BEGIN
		d := LONG(ORD(s[first]));
		d := d + LONG(ORD(s[first+1]))*100H;
		d := d + LONG(ORD(s[first+2]))*10000H;
		d := d + LONG(ORD(s[first+3]))*1000000H
	END Get733;

	(* Check - check filename.  Return correct name, or empty name if incorrect. *)
	PROCEDURE Check(s: ARRAY OF CHAR;  VAR name: Filename; VAR res: INTEGER);
	VAR i, j: LONGINT;  ch: CHAR;
	BEGIN
		IF nameDebug THEN LogString("Check: "); LogString(s) END;
		res := 0; i := 0;
		IF (s[0] = "/") OR (s[0] = "\") THEN j := 1 ELSE j := 0 END;		(* remove leading / or \ *)
		LOOP
			ch := s[j];
			IF ch = 0X THEN EXIT END;
			IF ch = "\" THEN ch := "/" END;
			IF (ch < " ") OR (ch >= 07FX) THEN res := eInvalidFileName; i := 0; EXIT END;
			IF (ch = "?") OR (ch = "*") THEN res := eNameIsWild END;
			name[i] := ch;
			INC(i); INC(j)
		END;
		name[i] := 0X;
		IF nameDebug THEN LogString(" => "); LogString(name); LogLn END;
	END Check;

	PROCEDURE GetVolumeDescriptors(fs: FileSystem; res: INTEGER);
	VAR b: ARRAY SS OF CHAR; i: LONGINT; vol: Files.Volume;
	BEGIN
		vol := fs.vol;
		i := 16; fs.pri := NIL; fs.sup := NIL; fs.cur := NIL; fs.jolietLevel := 0;
		REPEAT
			vol.GetBlock(i, b);	(* read boot sector *)
			CASE b[0] OF
				1X: (* Primary volume descriptor *)
					ASSERT(fs.pri = NIL, 102);	(* exactly one primary volume desc *)
					NEW(fs.pri);
					Get733(b, 158, fs.pri.root);	(* location of root directory *)
					Get733(b, 166, fs.pri.rootDirSize)	(* size of root directory in bytes *)
			|	2X: (* Supplementary volume descriptor  *)
					ASSERT(fs.sup = NIL, 103);	(* 0 or 1 supplementary volume descriptor *)
					ASSERT((b[88] = 25X) & (b[89] = 2FX) & ((b[90] = 40X) OR (b[90] = 43X) OR (b[90] = 45X)), 104);
					IF b[90] = 40X THEN fs.jolietLevel := 1
					ELSIF b[90] = 43X THEN fs.jolietLevel := 2
					ELSIF b[90] = 45X THEN fs.jolietLevel := 3
					END;
					NEW(fs.sup);
					Get733(b, 158, fs.sup.root);	(* location of root directory *)
					Get733(b, 166, fs.sup.rootDirSize)	(* size of root directory in bytes *)
			ELSE ASSERT((b[0] = 0X) OR (b[0] = 2X) OR (b[0] = 0FFX), 100)	(* boot or end *)
			END;
			INC(i)
		UNTIL (res # 0) OR (b[0] = 0FFX);
		IF res = 0 THEN
			IF fs.pri = NIL THEN res := eInvalidVolume
			ELSIF fs.sup # NIL THEN fs.cur := fs.sup; ExtractNameProc := ExtractLongName
			ELSE fs.cur := fs.pri; ExtractNameProc := ExtractShortName
			END
		END;
	END GetVolumeDescriptors;

	(* GetDir - Get information from a directory entry *)
	PROCEDURE GetDir(VAR dir, fname: ARRAY OF CHAR; VAR time, date, cl, len: LONGINT; VAR attr: SET);
	VAR t: LONGINT;
	BEGIN
		ExtractName(dir, fname);
		t (*attr*) := ORD(dir[24]); attr := SYSTEM.VAL(SET, t);
		time := LONG(ORD(dir[20]))*64*64 + LONG(ORD(dir[21]))*64 + LONG(ORD(dir[22]));
		date := LONG(ORD(dir[17]))*32*16 + LONG(ORD(dir[18]))*16 + LONG(ORD(dir[19]));
		Get733(dir, 1, cl);
		Get733(dir, 9, len)
	END GetDir;

	PROCEDURE SplitName(str: ARRAY OF CHAR; VAR prefix, name: ARRAY OF CHAR);
	VAR i, j: LONGINT;
	BEGIN
		IF nameDebug THEN LogString("SplitName: "); LogString(str) END;
		i := -1; j := -1;
		REPEAT INC(i); INC(j); prefix[j] := str[i] UNTIL (str[i] = 0X) OR (str[i] = "/");
		IF str[i] = "/" THEN
			prefix[j] := 0X; j := -1;
			REPEAT INC(i); INC(j); name[j] := str[i] UNTIL name[j] = 0X
		ELSE name[0] := 0X
		END;
		IF nameDebug THEN LogString(" => "); LogString(prefix); LogString(", "); LogString(name); LogLn END
	END SplitName;

	(* SeparateName - separate str into a prefix and a name. *)
	PROCEDURE SeparateName(str: ARRAY OF CHAR; VAR prefix: ARRAY OF CHAR; VAR name: Filename);
	VAR i, j : LONGINT;
	BEGIN
	(* Pre: str is result of a Check operation; all "\"s have been changed to "/" *)
		i := 0;  j := -1;
		WHILE str[i] # 0X DO
			IF str[i] = "/" THEN j := i END;
			INC(i)
		END;
		IF j >= 0 THEN
			COPY(str, prefix); prefix[j] := 0X;
			i := -1;
			REPEAT INC(i); INC(j); name[i] := str[j] UNTIL name[i] = 0X
		ELSE COPY(str, name); prefix[0] := 0X
		END
	END SeparateName;

	PROCEDURE ExtractShortName(VAR dir, name: ARRAY OF CHAR);
	VAR i, j, len: LONGINT;
	BEGIN
		len := ORD(dir[31]);
		i := 0;  j := 32;
		WHILE (i < len) & (dir[j] # ";") DO name[i] := dir[j];  INC(i); INC(j) END;
		name[i] := 0X;
	END ExtractShortName;

	PROCEDURE ExtractLongName(VAR dir, name: ARRAY OF CHAR);
	VAR i, j, end: LONGINT;
	BEGIN
		end := 33+ORD(dir[31]);
		i := 0;  j := 33;
		WHILE (j < end) & (dir[j] # ";") DO name[i] := dir[j];  INC(i); INC(j, 2) END;
		name[i] := 0X;
	END ExtractLongName;

	PROCEDURE ExtractName(VAR dir, name: ARRAY OF CHAR);
	VAR len: LONGINT;
	BEGIN
		len := ORD(dir[31]);
		IF len = 1 THEN
			IF dir[33] = 0X THEN COPY(".", name)
			ELSIF dir[33] = 1X THEN COPY("..", name)
			END
		ELSE ExtractNameProc(dir, name)
		END
	END ExtractName;

	PROCEDURE MatchFile(VAR R: Files.Rider; name: ARRAY OF CHAR; VAR fname: ARRAY OF CHAR;
		VAR pos, cl, time, date, len: LONGINT; VAR attr: SET; VAR res: INTEGER);
	VAR
		found: BOOLEAN; f: File; fs: FileSystem; buf: ARRAY 256 OF CHAR; entryLen, padding: LONGINT;
	BEGIN
		f := R.file(File); fs := R.fs(FileSystem);
		found := FALSE;
		LOOP
			pos := R.file.Pos(R);
			IF debug THEN LogString("MatchFile; pos: "); LogInt(pos); LogLn END;
			R.file.Read(R, buf[0]); entryLen := ORD(buf[0]);
			IF debug THEN LogString("MatchFile; entryLen: "); LogInt(entryLen); LogLn END;
			IF R.eof THEN
				IF debug THEN LogString("MatchFile; eof"); LogLn END;
				EXIT
			END;
			IF entryLen > 0 THEN
				R.file.ReadBytes(R, buf, 0, entryLen-1);
				GetDir(buf, fname, time, date, cl, len, attr);
				found := Strings.Match(name, fname);
			ELSE (* Directory entries may not cross sectors. Skip padding bytes *)
				padding := SS - (R.file.Pos(R) MOD SS);
				IF (padding > 0) & (padding <= 256) THEN
					IF debug THEN LogString("MatchFile; skip padding: "); LogInt(padding); LogLn; END;
					R.file.ReadBytes(R, buf, 0, padding); (* Skip padding *)
				ELSE
					IF debug THEN LogString("MatchFile; Confusing directory structure... givin' up"); END;
					EXIT;
				END;
			END;
			IF found THEN EXIT END;
		END;
		IF found THEN res := 0 ELSE res := eInvalidFileName END;
	END MatchFile;

	PROCEDURE FindFile(fs: FileSystem; name: ARRAY OF CHAR; dircl, dirlen: LONGINT;
		VAR dirpos, time, date, filecl, len: LONGINT; VAR attr: SET; VAR res: INTEGER);
	VAR f: File; R: Files.Rider; fname: Filename;
	BEGIN
		ASSERT(name # "", 100);
		f := OpenFile("", fs, -1, -1, -1, -1, dircl, dirlen, {});
		f.Set(R, 0);
		MatchFile(R, name, fname, dirpos, filecl, time, date, len, attr, res);
	END FindFile;

	PROCEDURE LocateFile(name: ARRAY OF CHAR; fs: FileSystem;
		VAR dircl, dirpos, time, date, filecl, len: LONGINT; VAR attr: SET; VAR res: INTEGER);
	VAR cur: Filename; dirlen: LONGINT;
	BEGIN
		res := 0;
		dircl := fs.cur.root; dirlen := fs.cur.rootDirSize;	(* start in root directory *)
		IF name[0] = 0X THEN (* root dir *)
			filecl := dircl; attr := {Directory};
			len := dirlen; dirpos := -1;
		ELSE
			LOOP
				SplitName(name, cur, name);
				FindFile(fs, cur, dircl, dirlen, dirpos, time, date, filecl, len, attr, res);
				IF (res = 0) & (name # "") & ~(Directory IN attr) THEN res := eInvalidFileName END;
				IF (res # 0) OR (name = "") THEN EXIT END;
				dircl := filecl; dirlen := len
			END
		END
	END LocateFile;

	PROCEDURE OpenFile(name: ARRAY OF CHAR; fs: FileSystem; dircl, dirpos, time, date, filecl, len: LONGINT; attr: SET): File;
	VAR f: File; buf: Buffer;
	BEGIN
		NEW(f); COPY(name, f.name); f.fs := fs; f.key := filecl;
		f.dircl := dircl; f.dirpos := dirpos; f.time := time; f.date := date;
		f.filecl := filecl; f.len := len; f.attr := attr;
		NEW(buf); buf.next := buf;
		NEW(buf.data, fs.vol.blockSize);
		IF f.len = 0 THEN buf.pos := 0; buf.lim := 0
		ELSE f.ReadBuf(buf, 0) (* file is not empty *)
		END;
		f.firstbuf := buf;  f.nofbufs := 1;
		RETURN f
	END OpenFile;

	PROCEDURE OldDir(fs: Files.FileSystem; name: ARRAY OF CHAR): Files.File;
	VAR f: File; dircl, dirpos, time, date, filecl, len: LONGINT; attr: SET; res: INTEGER;
	BEGIN
		res := 0; f := NIL;
		LocateFile(name, fs(FileSystem), dircl, dirpos, time, date, filecl, len, attr, res);
		IF res = 0 THEN
			IF ~(Directory IN attr) THEN res := eCannotOpenSubDir
			ELSIF filecl < 16 THEN res := eInvalidFirstCluster
			ELSE f := OpenFile(name, fs(FileSystem), dircl, dirpos, time, date, filecl, len, attr)
			END
		END;
		RETURN f
	END OldDir;

(** Generate a new file system object.  OFS.NewVol has volume parameter, OFS.Par has mount prefix. *)
PROCEDURE NewFS*(context : Files.Parameters);
VAR fs: FileSystem; res: INTEGER;
BEGIN
	IF Files.This(context.prefix) = NIL THEN
		NEW(fs);  fs.vol := context.vol;
		GetVolumeDescriptors(fs, res);
		IF res = 0 THEN
			IF debug THEN
				LogString("  primary root: "); LogInt(fs.pri.root); LogLn;
				LogString("  primary root dir size: "); LogInt(fs.pri.rootDirSize); LogLn
			END;
			fs.desc[0] := 0X;  Files.AppendStr(context.vol.name, fs.desc);  Files.AppendStr(" / ISO9660FS", fs.desc);
			Files.Add(fs, context.prefix)
		ELSE context.error.String("No ISO9660 file system found on "); context.error.String(context.vol.name); context.error.Ln;
		END
	ELSE context.error.String(context.prefix); context.error.String(" already in use"); context.error.Ln;
	END;
END NewFS;

(* Clean up when module freed. *)
PROCEDURE Cleanup;
VAR ft: Files.FileSystemTable; i: LONGINT;
BEGIN
	IF Modules.shutdown = Modules.None THEN
		Files.GetList(ft);
		IF ft # NIL THEN
			FOR i := 0 TO LEN(ft^)-1 DO
				IF ft[i] IS FileSystem THEN Files.Remove(ft[i]) END
			END
		END
	END
END Cleanup;

BEGIN
	Modules.InstallTermHandler(Cleanup)
END ISO9660Files.

ISO9660Volumes.Mod

Partitions.Show

OFSTools.Mount CD IsoFS  IDE2 ~
OFSTools.Unmount CD0 ~

SystemTools.Free ISO9660Files  ISO9660Volumes ~

System.Directory CD:*.* ~

History:

30.05.2006	Fixed MatchFile: Directory entries may not cross sector boundaries. Skip padding bytes if necessary. (staubesv)