Skip to content

Commit

Permalink
Make SylkParser more robust to handle malformed files & ignore useles…
Browse files Browse the repository at this point in the history
…s tags, like formatting.
  • Loading branch information
speige committed Jan 5, 2025
1 parent d9359e6 commit f21bf4e
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 79 deletions.
211 changes: 135 additions & 76 deletions src/War3Net.IO.Slk/SylkParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,138 +6,197 @@
// ------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;

namespace War3Net.IO.Slk
{
public sealed class SylkParser
{
private SylkTable _table;
private int? _lastY;

public SylkParser()
public SylkTable Parse(Stream input, bool leaveOpen = false)
{
_lastY = null;
var lines = new List<string>();
using var reader = new StreamReader(input, Encoding.UTF8, true, 1024, leaveOpen);
{
string line;
while ((line = reader.ReadLine()) != null)
{
lines.Add(line);
}
}

return Parse(lines);
}

public SylkTable Parse(Stream input, bool leaveOpen = false)
public SylkTable Parse(List<string> lines)
{
using var reader = new StreamReader(input, Encoding.UTF8, true, 1024, leaveOpen);
int? maxX = null;
int? maxY = null;

var isOnFirstLine = true;
while (true)
var bLine = lines.FirstOrDefault(x => x.StartsWith("B;", StringComparison.InvariantCultureIgnoreCase));
if (!string.IsNullOrWhiteSpace(bLine))
{
var line = reader.ReadLine();
var fields = line.Split(';');
var recordType = fields[0];

string GetField(string fieldName, bool mandatory)
var parts = bLine.Split(';', StringSplitOptions.TrimEntries);
foreach (var part in parts)
{
foreach (var field in fields)
if (part.StartsWith("X", StringComparison.InvariantCultureIgnoreCase))
{
if (field.StartsWith(fieldName))
if (int.TryParse(part.Substring(1), out int x))
{
return field.Substring(fieldName.Length);
maxX = x;
}
}

if (mandatory)
else if (part.StartsWith("Y", StringComparison.InvariantCultureIgnoreCase))
{
throw new InvalidDataException($"Record does not contain mandatory field of type '{fieldName}'.");
if (int.TryParse(part.Substring(1), out int y))
{
maxY = y;
}
}
else if (part.StartsWith("D", StringComparison.InvariantCultureIgnoreCase))
{
var dContent = part.Substring(1).Trim();
var dParts = dContent.Split(' ');
if (dParts.Length == 2)
{
if (int.TryParse(dParts[0], out int y))
{
maxY ??= y;
}
if (int.TryParse(dParts[1], out int x))
{
maxX ??= x;
}
}
else if (dParts.Length == 4)
{
int.TryParse(dParts[0], out int startY);
int.TryParse(dParts[1], out int startX);
if (int.TryParse(dParts[2], out int y))
{
maxY ??= (y - startY);
}
if (int.TryParse(dParts[3], out int x))
{
maxX ??= (x - startX);
}
}
}

return null;
}
}

if (isOnFirstLine)
var _table = new SylkTable(maxX ?? 0, maxY ?? 0);


int nextX = 0;
int nextY = 0;

foreach (var line in lines)
{
var isCell = line.StartsWith("C;", StringComparison.InvariantCultureIgnoreCase);
var isFormatting = line.StartsWith("F;", StringComparison.InvariantCultureIgnoreCase);
if (isCell || isFormatting)
{
isOnFirstLine = false;
if (recordType != "ID")
int? x = null;
int? y = null;
object value = null;

var parts = line.Split(";", StringSplitOptions.TrimEntries);
foreach (var part in parts)
{
throw new InvalidDataException("SYLK file must start with 'ID'.");
if (part.StartsWith("X", StringComparison.InvariantCultureIgnoreCase))
{
if (int.TryParse(part.Substring(1), out int parsedX))
{
x = parsedX - 1;
}
}
else if (part.StartsWith("Y", StringComparison.InvariantCultureIgnoreCase))
{
if (int.TryParse(part.Substring(1), out int parsedY))
{
y = parsedY - 1;
}
}
else if (part.StartsWith("K", StringComparison.InvariantCultureIgnoreCase))
{
value = ParseValueString(part.Substring(1));
}
}

GetField("P", true);
}
else
{
switch (recordType)
if (isFormatting && x == null && y == null)
{
case "ID":
throw new InvalidDataException("Record type 'ID' can only occur on the first line.");
continue;
}

case "B":
if (_table != null)
{
throw new InvalidDataException("Only one record of type 'B' may be present.");
}
x ??= nextX;
y ??= nextY;

_table = new SylkTable(int.Parse(GetField("X", true)), int.Parse(GetField("Y", true)));
break;
nextX = x.Value;
nextY = y.Value;

case "C":
if (_table == null)
{
throw new InvalidDataException("Unable to parse record of type 'C' before encountering a record of type 'B'.");
}
if (isCell)
{
nextX++;
}

SetCellContent(GetField("X", true), GetField("Y", false), GetField("K", false));
break;
if (maxX.HasValue && nextX >= maxX)
{
nextX = 0;
nextY++;
}

case "E":
return _table;
if (value != null)
{
if (x.Value >= _table.Width || y.Value >= _table.Height)
{
_table.Resize(Math.Max(x.Value+1, _table.Width), Math.Max(y.Value+1, _table.Height));
}

default:
throw new NotSupportedException($"Support for record type '{recordType}' is not implemented. Only records of type 'ID', 'B', 'C', and 'E' are supported.");
_table[x.Value, y.Value] = value;
}
}
}

return _table;
}

/// <param name="x">The cell's 1-indexed X position.</param>
/// <param name="y">The cell's 1-indexed Y position.</param>
private void SetCellContent(string x, string? y, string value)
private object ParseValueString(string value)
{
if (y == null && _lastY == null)
if (value.StartsWith('"'))
{
throw new InvalidDataException("Row for cell is not defined.");
}

var xi = int.Parse(x, NumberStyles.Integer, CultureInfo.InvariantCulture) - 1;
var yi = y == null ? _lastY.Value : (int.Parse(y, NumberStyles.Integer, CultureInfo.InvariantCulture) - 1);
if (!value.EndsWith('"'))
{
value = value + "\"";
}

if (value.StartsWith('"') && value.EndsWith('"'))
{
_table[xi, yi] = value[1..^1];
return value.Substring(1, value.Length - 2);
}
else if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var @int))
else if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
_table[xi, yi] = @int;
return intValue;
}
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var @float))
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var floatValue))
{
_table[xi, yi] = @float;
return floatValue;
}
else if (string.Equals(value, bool.TrueString, StringComparison.OrdinalIgnoreCase))
{
_table[xi, yi] = true;
return true;
}
else if (string.Equals(value, bool.FalseString, StringComparison.OrdinalIgnoreCase))
{
_table[xi, yi] = false;
}
else if (string.Equals(value, "#VALUE!", StringComparison.Ordinal) || string.Equals(value, "#REF!", StringComparison.Ordinal))
{
_table[xi, yi] = 0;
return false;
}
else
else if (string.Equals(value, "#VALUE!", StringComparison.Ordinal) || string.Equals(value, "#REF!", StringComparison.Ordinal) || string.IsNullOrEmpty(value))
{
throw new NotSupportedException($"Unable to parse value '{value}'. Can only parse strings, integers, floats, and booleans.");
return 0;
}

_lastY = yi;
return null;
}
}
}
28 changes: 25 additions & 3 deletions src/War3Net.IO.Slk/SylkTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ namespace War3Net.IO.Slk
{
public sealed class SylkTable : IEnumerable<object[]>
{
private readonly int _width;
private readonly int _height;
private readonly object[,] _values;
private int _width;
private int _height;
private object[,] _values;

private int _rows;
private int _columns;
Expand All @@ -28,6 +28,28 @@ public SylkTable(int width, int height)
_values = new object[_width, _height];
}

public void Resize(int width, int height)
{
var oldWidth = _width;
var oldHeight = _height;
var oldValues = _values;

_width = width;
_height = height;
_values = new object[_width, _height];

for (int column = 0; column < Math.Min(_width, oldWidth); column++)
{
for (int row = 0; row < Math.Min(height, oldHeight); row++)
{
_values[column, row] = oldValues[column, row];
}
}

_rows = Math.Min(_rows, _height-1);
_columns = Math.Min(_columns, _width-1);
}

/// <summary>
/// Gets the maximum amount of columns that can fit in the <see cref="SylkTable"/>.
/// </summary>
Expand Down

0 comments on commit f21bf4e

Please sign in to comment.