Commit 59c20d32 authored by Jesse McGrew's avatar Jesse McGrew
Browse files

Refactor filesystem access to go through IFileSystem interface.

parent 13f99c3fa6f8
......@@ -25,11 +25,10 @@ using System.Text;
using Zilf.Common.StringEncoding;
using Zapf.Parsing.Diagnostics;
using Zapf.Parsing.Instructions;
using Zilf.Common;
namespace Zapf
{
delegate Stream OpenFileDelegate(string filename, bool writing);
delegate bool FileExistsDelegate(string filename);
delegate IDebugFileWriter GetDebugWriterDelegate(Stream stream);
class Context : IErrorSink, IDisposable
......@@ -76,12 +75,9 @@ namespace Zapf
public int? TableStart, TableSize;
public bool InVocab;
public OpenFileDelegate? InterceptOpenFile;
public FileExistsDelegate? InterceptFileExists;
public IFileSystem FileSystem { get; set; } = PhysicalFileSystem.Instance;
#pragma warning disable CS0649
public GetDebugWriterDelegate? InterceptGetDebugWriter;
#pragma warning restore CS0649
char? LanguageEscapeChar { get; set; }
......@@ -327,8 +323,10 @@ namespace Zapf
if (InVocab)
{
// restore stream
var buffer = ((MemoryStream?)stream)!.GetBuffer();
var bufLen = (int)stream.Length;
if (!((MemoryStream)stream!).TryGetBuffer(out var bufferSegment))
throw new InvalidOperationException("Can't access vocab buffer");
var buffer = bufferSegment.AsSpan();
stream = prevStream;
prevStream = null;
......@@ -336,8 +334,8 @@ namespace Zapf
// sort vocab words
// we use an insertion sort because ZILF's vocab table is mostly sorted already
int records = bufLen / vocabRecSize;
var temp = new byte[vocabRecSize];
int records = buffer.Length / vocabRecSize;
Span<byte> temp = stackalloc byte[vocabRecSize];
var newIndexes = new int[records];
newIndexes[0] = 0;
......@@ -346,15 +344,17 @@ namespace Zapf
{
if (VocabCompare(buffer, i - 1, i) > 0)
{
VocabRecord(buffer, i).CopyTo(temp);
var home = VocabSearch(buffer, i - 1, i);
Array.Copy(buffer, i * vocabRecSize, temp, 0, vocabRecSize);
Array.Copy(buffer, home * vocabRecSize, buffer, (home + 1) * vocabRecSize,
(i - home) * vocabRecSize);
Array.Copy(temp, 0, buffer, home * vocabRecSize, vocabRecSize);
VocabMove(buffer, home, home + 1, i - home);
temp.CopyTo(VocabRecord(buffer, home));
for (int j = 0; j < i; j++)
if (newIndexes[j] >= home && newIndexes[j] < i)
newIndexes[j]++;
newIndexes[i] = home;
}
else
......@@ -388,7 +388,7 @@ namespace Zapf
}
}
stream?.Write(buffer, 0, bufLen);
stream?.Write(buffer);
// apply fixups
var vocabEnd = position;
......@@ -419,6 +419,20 @@ namespace Zapf
vocabKeySize = 0;
}
void VocabMove(Span<byte> buffer, int srcIndex, int destIndex, int recordCount)
{
var byteCount = recordCount * vocabRecSize;
var src = buffer.Slice(srcIndex * vocabRecSize, byteCount);
var dest = buffer.Slice(destIndex * vocabRecSize, byteCount);
src.CopyTo(dest);
}
Span<byte> VocabRecord(Span<byte> buffer, int index) =>
buffer.Slice(index * vocabRecSize, vocabRecSize);
ReadOnlySpan<byte> VocabRecord(ReadOnlySpan<byte> buffer, int index) =>
buffer.Slice(index * vocabRecSize, vocabRecSize);
int MapVocabAddress(int oldAddress, int[] newIndexes)
{
var oldOffsetFromVocab = oldAddress - vocabStart;
......@@ -428,7 +442,7 @@ namespace Zapf
return vocabStart + (newIndex * vocabRecSize) + offsetWithinEntry;
}
int VocabSearch(byte[] buffer, int numRecs, int keyRec)
int VocabSearch(ReadOnlySpan<byte> buffer, int numRecs, int keyRec)
{
int start = 0, end = numRecs - 1;
while (start <= end)
......@@ -446,20 +460,8 @@ namespace Zapf
return start;
}
int VocabCompare(byte[] buffer, int idx1, int idx2)
{
idx1 *= vocabRecSize;
idx2 *= vocabRecSize;
for (int i = 0; i < vocabKeySize; i++)
{
int diff = buffer[idx1 + i] - buffer[idx2 + i];
if (diff != 0)
return diff;
}
return 0;
}
int VocabCompare(ReadOnlySpan<byte> buffer, int idx1, int idx2) =>
VocabRecord(buffer, idx1).SequenceCompareTo(VocabRecord(buffer, idx2));
string? VocabLabel(int index)
{
......@@ -497,7 +499,7 @@ namespace Zapf
OutFile = Path.ChangeExtension(OutFile, ".z" + ZVersion);
position = 0;
stream = OpenFile(OutFile, true);
stream = FileSystem.OpenForWriting(OutFile);
}
public void CloseOutput()
......@@ -510,7 +512,7 @@ namespace Zapf
{
Debug.Assert(DebugFile != null);
var debugStream = OpenFile(DebugFile, true);
var debugStream = FileSystem.OpenForWriting(DebugFile);
if (InterceptGetDebugWriter != null)
{
......@@ -850,36 +852,17 @@ namespace Zapf
Console.Error.WriteLine("fatal error: {0}", fer.Message);
}
public Stream OpenFile(string filename, bool writing)
{
var intercept = InterceptOpenFile;
if (intercept != null)
return intercept(filename, writing);
return new FileStream(
filename,
writing ? FileMode.Create : FileMode.Open,
writing ? FileAccess.ReadWrite : FileAccess.Read);
}
public bool FileExists(string filename)
{
var intercept = InterceptFileExists;
return intercept?.Invoke(filename) ?? File.Exists(filename);
}
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
public string? FindInsertedFile(string name)
{
if (FileExists(name))
if (FileSystem.Exists(name))
return name;
string search = name + ".zap";
if (FileExists(search))
if (FileSystem.Exists(search))
return search;
search = name + ".xzap";
if (FileExists(search))
if (FileSystem.Exists(search))
return search;
return null;
......@@ -973,7 +956,7 @@ namespace Zapf
Object,
}
class Symbol
sealed class Symbol
{
/// <summary>
/// The symbol's name in the source code.
......@@ -1011,7 +994,7 @@ namespace Zapf
}
}
class Fixup
sealed class Fixup
{
public Fixup(string symbol) => Symbol = symbol;
......
......@@ -29,6 +29,7 @@ using Zapf.Parsing.Diagnostics;
using Zapf.Parsing.Directives;
using Zapf.Parsing.Expressions;
using Zapf.Parsing.Instructions;
using System.Diagnostics.CodeAnalysis;
namespace Zapf
{
......@@ -40,8 +41,7 @@ namespace Zapf
public static int Main(string[] args)
{
var ctx = ParseArgs(args);
if (ctx == null)
if (!TryParseArgs(args, out var ctx))
{
Usage();
return 1;
......@@ -231,82 +231,95 @@ namespace Zapf
return result;
}
internal static Context? ParseArgs(IReadOnlyList<string> args)
internal static bool TryParseArgs(IReadOnlyList<string> args, [NotNullWhen(true)] out Context? ctx)
{
using var result = new Context();
var result = new Context();
if (TryParseArgs(args, result))
{
ctx = result;
return true;
}
ctx = null;
return false;
}
internal static bool TryParseArgs(IReadOnlyList<string> args, Context ctx)
{
string? inFile = null, outFile = null;
for (int i = 0; i < args.Count; i++)
{
switch (args[i])
{
case "-ab":
result.AbbreviateMode = true;
ctx.AbbreviateMode = true;
break;
case "-q":
result.Quiet = true;
ctx.Quiet = true;
break;
case "-i":
result.InformMode = true;
ctx.InformMode = true;
break;
case "-la":
result.ListAddresses = true;
ctx.ListAddresses = true;
break;
case "-r":
if (++i == args.Count)
return null;
result.Release = short.Parse(args[i]);
return false;
ctx.Release = short.Parse(args[i]);
break;
case "-s":
if (++i == args.Count)
return null;
result.Serial = args[i];
return false;
ctx.Serial = args[i];
break;
case "-c":
if (++i == args.Count)
return null;
result.Creator = args[i];
return false;
ctx.Creator = args[i];
break;
case "-c0":
result.Creator = null;
ctx.Creator = null;
break;
case "-dx":
result.XmlDebugMode = true;
ctx.XmlDebugMode = true;
break;
case "-?":
case "--help":
case "/?":
return null;
return false;
default:
if (result.InFile == null)
result.InFile = args[i];
else if (result.OutFile == null)
result.OutFile = args[i];
if (inFile == null)
inFile = args[i];
else if (outFile == null)
outFile = args[i];
else
return null;
return false;
break;
}
}
// validate
if (result.InFile == null)
return null;
if (result.OutFile == null)
result.OutFile = Path.ChangeExtension(result.InFile, ".z#");
if (inFile == null)
return false;
result.DebugFile = Path.ChangeExtension(result.OutFile, result.XmlDebugMode ? ".dbg.xml" : ".dbg");
ctx.InFile = inFile;
ctx.OutFile = outFile ?? Path.ChangeExtension(ctx.InFile, ".z#");
ctx.DebugFile = Path.ChangeExtension(ctx.OutFile, ctx.XmlDebugMode ? ".dbg.xml" : ".dbg");
return result;
return true;
}
static void Usage()
......@@ -806,7 +819,8 @@ General switches:
static IEnumerable<AsmLine> ReadRootsFromFile(Context ctx, string path)
{
using var stream = ctx.OpenFile(path, false);
using var stream = ctx.FileSystem.OpenForReading(path);
Debug.Assert(ctx.OpcodeDict != null);
var parser = new ZapParser(ctx, ctx.OpcodeDict);
var result = parser.Parse(stream, path);
......
......@@ -21,9 +21,11 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using Zapf.Parsing;
using Zapf.Parsing.Diagnostics;
using Zilf.Common;
namespace Zapf
{
[Obsolete("Use " + nameof(IFileSystem) + " instead.")]
class OpeningFileEventArgs : EventArgs
{
public OpeningFileEventArgs(string filename, bool writing)
......@@ -39,6 +41,7 @@ namespace Zapf
public Stream? Stream { get; set; }
}
[Obsolete("Use " + nameof(IFileSystem) + " instead.")]
class CheckingFilePresenceEventArgs : EventArgs
{
public CheckingFilePresenceEventArgs(string filename) => FileName = filename;
......@@ -52,7 +55,7 @@ namespace Zapf
{
public InitializingContextEventArgs(Context ctx) => Context = ctx;
public Context Context { get; set; }
public Context Context { get; }
}
readonly struct AssemblyResult
......@@ -72,44 +75,9 @@ namespace Zapf
sealed class ZapfAssembler
{
public event EventHandler<OpeningFileEventArgs>? OpeningFile;
public event EventHandler<CheckingFilePresenceEventArgs>? CheckingFilePresence;
public event EventHandler<InitializingContextEventArgs>? InitializingContext;
Stream OpenFile(string path, bool writing)
{
var handler = OpeningFile;
if (handler != null)
{
var args = new OpeningFileEventArgs(path, writing);
handler(this, args);
if (args.Stream != null)
return args.Stream;
}
return new FileStream(
path,
writing ? FileMode.Create : FileMode.Open,
writing ? FileAccess.ReadWrite : FileAccess.Read);
}
public IFileSystem FileSystem { get; set; } = PhysicalFileSystem.Instance;
bool CheckFileExists(string path)
{
var handler = CheckingFilePresence;
if (handler != null)
{
var args = new CheckingFilePresenceEventArgs(path);
handler(this, args);
if (args.Exists.HasValue)
return args.Exists.Value;
}
return File.Exists(path);
}
public event EventHandler<InitializingContextEventArgs>? InitializingContext;
Context InitializeContext(string inputFileName, string? outputFileName)
{
......@@ -118,7 +86,8 @@ namespace Zapf
Quiet = true,
InFile = inputFileName,
OutFile = outputFileName,
DebugFile = Path.ChangeExtension(outputFileName, ".dbg")
DebugFile = Path.ChangeExtension(outputFileName, ".dbg"),
FileSystem = FileSystem
};
var handler = InitializingContext;
......@@ -126,12 +95,8 @@ namespace Zapf
{
var args = new InitializingContextEventArgs(ctx);
handler(this, args);
ctx = args.Context;
}
ctx.InterceptOpenFile = OpenFile;
ctx.InterceptFileExists = CheckFileExists;
return ctx;
}
......
/* Copyright 2010-2018 Jesse McGrew
*
* This file is part of ZILF.
*
* ZILF 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 3 of the License, or
* (at your option) any later version.
*
* ZILF 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 ZILF. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Zilf.Common
{
/// <summary>
/// Exposes a logical file system to the compiler and interpreter.
/// </summary>
public interface IFileSystem
{
/// <summary>
/// Tests whether a file currently exists.
/// </summary>
/// <param name="path">The file path.</param>
/// <returns><see langword="true"/> if the file currently exists; otherwise, <see langword="false"/>.</returns>
/// <remarks>
/// This method does not test whether the file can be read or written.
/// The file may also be created or deleted by another process after this method is called.
/// </remarks>
bool Exists(string path);
/// <summary>
/// Opens a file in read-only mode.
/// </summary>
/// <param name="path">The file path.</param>
/// <returns>A stream.</returns>
/// <exception cref="FileNotFoundException">The file does not exist.</exception>
/// <exception cref="IOException">The file could not be opened.</exception>
Stream OpenForReading(string path);
/// <summary>
/// Opens a file in write-only mode.
/// </summary>
/// <param name="path">The file path.</param>
/// <returns>A stream.</returns>
/// <exception cref="NotSupportedException">The file system does not support writing.</exception>
/// <exception cref="IOException">The file could not be opened.</exception>
/// <remarks>If the file already exists, it will be truncated.</remarks>
Stream OpenForWriting(string path);
/// <summary>
/// Opens a file in write mode with the ability to seek and re-read data that has already been written.
/// </summary>
/// <param name="path">The file path.</param>
/// <returns>A stream.</returns>
/// <exception cref="NotSupportedException">The file system does not support writing.</exception>
/// <exception cref="IOException">The file could not be opened.</exception>
/// <remarks>If the file already exists, it will be truncated.</remarks>
Stream OpenForWritingAndReading(string path);
/// <summary>
/// Gets the contents of a readable file as a byte array.
/// </summary>
/// <param name="path">The file path.</param>
/// <returns>A byte array containing the file contents.</returns>
/// <exception cref="FileNotFoundException">The file does not exist.</exception>
/// <exception cref="IOException">The file could not be read.</exception>
byte[] GetBytes(string path)
{
using var input = OpenForReading(path);
using var output = new MemoryStream();
input.CopyTo(output);
return output.ToArray();
}
/// <summary>
/// Asynchronously gets the contents of a readable file as a byte array, using a specified cancellation token.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
/// <exception cref="FileNotFoundException">The file does not exist.</exception>
/// <exception cref="IOException">The file could not be read.</exception>
async Task<byte[]> GetBytesAsync(string path, CancellationToken cancellationToken = default)
{
var input = OpenForReading(path);
await using var _ = input.ConfigureAwait(false);
using var output = new MemoryStream();
await input.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
return output.ToArray();
}
/// <summary>
/// Gets the contents of a readable file as a string, using UTF-8 encoding.
/// </summary>
/// <param name="path">The file path.</param>
/// <returns>A string containing the file contents.</returns>
/// <exception cref="FileNotFoundException">The file does not exist.</exception>
/// <exception cref="IOException">The file could not be read.</exception>
string GetText(string path)
{
using var input = OpenForReading(path);
using var rdr = new StreamReader(input, Encoding.UTF8);
return rdr.ReadToEnd();
}
/// <summary>
/// Asynchronously gets the contents of a readable file as a string, using UTF-8 encoding.
/// </summary>
/// <param name="path">The file path.</param>
/// <returns>A task that represents the asynchronous read operation.</returns>
/// <exception cref="FileNotFoundException">The file does not exist.</exception>
/// <exception cref="IOException">The file could not be read.</exception>
async Task<string> GetTextAsync(string path)
{
var input = OpenForReading(path);
await using var _ = input.ConfigureAwait(false);
using var rdr = new StreamReader(input, Encoding.UTF8);
return await rdr.ReadToEndAsync().ConfigureAwait(false);
}
/// <summary>
/// Sets the contents of a writable file from a string, using UTF-8 encoding.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="content">A string containing the desired contents of the file.</param>
/// <exception cref="NotSupportedException">The file system does not support writing.</exception>
/// <exception cref="IOException">The file could not be written.</exception>
void SetText(string path, string content)
{
using var output = OpenForWriting(path);
using var wtr = new StreamWriter(output, Encoding.UTF8);
wtr.Write(content);
}
/// <summary>
/// Asynchronously sets the contents of a writable file from a string, using UTF-8 encoding.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="content">A string containing the desired contents of the file.</param>
/// <returns>A task representing the asynchronous write operation.</returns>
/// <exception cref="NotSupportedException">The file system does not support writing.</exception>
/// <exception cref="IOException">The file could not be written.</exception>
async Task SetTextAsync(string path, string content)
{
var output = OpenForWriting(path);
await using var _ = output.ConfigureAwait(false);
var wtr = new StreamWriter(output, Encoding.UTF8);
await using var __ = wtr.ConfigureAwait(false);
await wtr.WriteAsync(content).ConfigureAwait(false);
}
}