mirror of
https://github.com/claunia/SabreTools.git
synced 2025-12-16 19:14:27 +00:00
[ClrMamePro] Fix and use new parser
This commit is contained in:
@@ -59,561 +59,6 @@ namespace SabreTools.Library.DatFiles
|
||||
Encoding enc = Utilities.GetEncoding(filename);
|
||||
StreamReader sr = new StreamReader(Utilities.TryOpenRead(filename), enc);
|
||||
|
||||
bool block = false, superdat = false, containsItems = false;
|
||||
string blockname = "", tempgamename = "", gamedesc = "", cloneof = "",
|
||||
romof = "", sampleof = "", year = "", manufacturer = "";
|
||||
while (!sr.EndOfStream)
|
||||
{
|
||||
string line = sr.ReadLine();
|
||||
|
||||
// Comments in CMP DATs start with a #
|
||||
if (line.Trim().StartsWith("#"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the line is the header or a game
|
||||
if (Regex.IsMatch(line, Constants.HeaderPatternCMP))
|
||||
{
|
||||
GroupCollection gc = Regex.Match(line, Constants.HeaderPatternCMP).Groups;
|
||||
|
||||
if (gc[1].Value == "clrmamepro" || gc[1].Value == "romvault" || gc[1].Value.ToLowerInvariant() == "doscenter")
|
||||
{
|
||||
blockname = "header";
|
||||
}
|
||||
|
||||
block = true;
|
||||
containsItems = false;
|
||||
}
|
||||
|
||||
// If the line is a rom-like item and we're in a block
|
||||
else if ((line.Trim().StartsWith("rom (")
|
||||
|| line.Trim().StartsWith("disk (")
|
||||
|| line.Trim().StartsWith("file (")
|
||||
|| (line.Trim().StartsWith("sample") && !line.Trim().StartsWith("sampleof"))
|
||||
) && block)
|
||||
{
|
||||
containsItems = true;
|
||||
ItemType temptype = ItemType.Rom;
|
||||
if (line.Trim().StartsWith("rom ("))
|
||||
{
|
||||
temptype = ItemType.Rom;
|
||||
}
|
||||
else if (line.Trim().StartsWith("disk ("))
|
||||
{
|
||||
temptype = ItemType.Disk;
|
||||
}
|
||||
else if (line.Trim().StartsWith("file ("))
|
||||
{
|
||||
temptype = ItemType.Rom;
|
||||
}
|
||||
else if (line.Trim().StartsWith("sample"))
|
||||
{
|
||||
temptype = ItemType.Sample;
|
||||
}
|
||||
|
||||
// Create the proper DatItem based on the type
|
||||
DatItem item = Utilities.GetDatItem(temptype);
|
||||
|
||||
// Then populate it with information
|
||||
item.MachineName = tempgamename;
|
||||
item.MachineDescription = gamedesc;
|
||||
item.CloneOf = cloneof;
|
||||
item.RomOf = romof;
|
||||
item.SampleOf = sampleof;
|
||||
item.Manufacturer = manufacturer;
|
||||
item.Year = year;
|
||||
|
||||
item.SystemID = sysid;
|
||||
item.SourceID = srcid;
|
||||
|
||||
// If we have a sample, treat it special
|
||||
if (temptype == ItemType.Sample)
|
||||
{
|
||||
line = line.Trim().Remove(0, 6).Trim().Replace("\"", ""); // Remove "sample" from the input string
|
||||
item.Name = line;
|
||||
|
||||
// Now process and add the sample
|
||||
ParseAddHelper(item, clean, remUnicode);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the line split by spaces and quotes
|
||||
string[] gc = Utilities.SplitLineAsCMP(line);
|
||||
|
||||
// Special cases for DOSCenter DATs only because of how the lines are arranged
|
||||
if (line.Trim().StartsWith("file ("))
|
||||
{
|
||||
// Loop over the specifics
|
||||
for (int i = 0; i < gc.Length; i++)
|
||||
{
|
||||
// Names are not quoted, for some stupid reason
|
||||
if (gc[i] == "name")
|
||||
{
|
||||
// Get the name in order until we find the next flag
|
||||
while (++i < gc.Length && gc[i] != "size" && gc[i] != "date" && gc[i] != "crc" && gc[i] != "md5"
|
||||
&& gc[i] != "sha1" && gc[i] != "sha256" && gc[i] != "sha384" && gc[i] != "sha512")
|
||||
{
|
||||
item.Name += " " + gc[i];
|
||||
}
|
||||
|
||||
// Perform correction
|
||||
item.Name = item.Name.TrimStart();
|
||||
i--;
|
||||
}
|
||||
|
||||
// Get the size from the next part
|
||||
else if (gc[i] == "size")
|
||||
{
|
||||
long tempsize = -1;
|
||||
if (!Int64.TryParse(gc[++i], out tempsize))
|
||||
{
|
||||
tempsize = 0;
|
||||
}
|
||||
((Rom)item).Size = tempsize;
|
||||
}
|
||||
|
||||
// Get the date from the next part
|
||||
else if (gc[i] == "date")
|
||||
{
|
||||
((Rom)item).Date = gc[++i].Replace("\"", "") + " " + gc[++i].Replace("\"", "");
|
||||
}
|
||||
|
||||
// Get the CRC from the next part
|
||||
else if (gc[i] == "crc")
|
||||
{
|
||||
((Rom)item).CRC = gc[++i].Replace("\"", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Get the MD5 from the next part
|
||||
else if (gc[i] == "md5")
|
||||
{
|
||||
((Rom)item).MD5 = gc[++i].Replace("\"", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Get the SHA1 from the next part
|
||||
else if (gc[i] == "sha1")
|
||||
{
|
||||
((Rom)item).SHA1 = gc[++i].Replace("\"", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Get the SHA256 from the next part
|
||||
else if (gc[i] == "sha256")
|
||||
{
|
||||
((Rom)item).SHA256 = gc[++i].Replace("\"", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Get the SHA384 from the next part
|
||||
else if (gc[i] == "sha384")
|
||||
{
|
||||
((Rom)item).SHA384 = gc[++i].Replace("\"", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Get the SHA512 from the next part
|
||||
else if (gc[i] == "sha512")
|
||||
{
|
||||
((Rom)item).SHA512 = gc[++i].Replace("\"", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
// Now process and add the rom
|
||||
ParseAddHelper(item, clean, remUnicode);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Loop over all attributes normally and add them if possible
|
||||
for (int i = 0; i < gc.Length; i++)
|
||||
{
|
||||
// Look at the current item and use it if possible
|
||||
string quoteless = gc[i].Replace("\"", "");
|
||||
switch (quoteless)
|
||||
{
|
||||
//If the item is empty, we automatically skip it because it's a fluke
|
||||
case "":
|
||||
continue;
|
||||
|
||||
// Special cases for standalone item statuses
|
||||
case "baddump":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
((Rom)item).ItemStatus = ItemStatus.BadDump;
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
((Disk)item).ItemStatus = ItemStatus.BadDump;
|
||||
}
|
||||
break;
|
||||
case "good":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
((Rom)item).ItemStatus = ItemStatus.Good;
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
((Disk)item).ItemStatus = ItemStatus.Good;
|
||||
}
|
||||
break;
|
||||
case "nodump":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
((Rom)item).ItemStatus = ItemStatus.Nodump;
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
((Disk)item).ItemStatus = ItemStatus.Nodump;
|
||||
}
|
||||
break;
|
||||
case "verified":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
((Rom)item).ItemStatus = ItemStatus.Verified;
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
((Disk)item).ItemStatus = ItemStatus.Verified;
|
||||
}
|
||||
break;
|
||||
|
||||
// Regular attributes
|
||||
case "name":
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
item.Name = quoteless;
|
||||
break;
|
||||
case "size":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
if (Int64.TryParse(quoteless, out long size))
|
||||
{
|
||||
((Rom)item).Size = size;
|
||||
}
|
||||
else
|
||||
{
|
||||
((Rom)item).Size = -1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "crc":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Rom)item).CRC = quoteless.ToLowerInvariant();
|
||||
}
|
||||
break;
|
||||
case "md5":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Rom)item).MD5 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
i++;
|
||||
quoteless = gc[i].Replace("\"", "");
|
||||
((Disk)item).MD5 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
break;
|
||||
case "sha1":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Rom)item).SHA1 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Disk)item).SHA1 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
break;
|
||||
case "sha256":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Rom)item).SHA256 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Disk)item).SHA256 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
break;
|
||||
case "sha384":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Rom)item).SHA384 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Disk)item).SHA384 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
break;
|
||||
case "sha512":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Rom)item).SHA512 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
((Disk)item).SHA512 = quoteless.ToLowerInvariant();
|
||||
}
|
||||
break;
|
||||
case "status":
|
||||
case "flags":
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
if (quoteless.ToLowerInvariant() == "good")
|
||||
{
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
((Rom)item).ItemStatus = ItemStatus.Good;
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
((Disk)item).ItemStatus = ItemStatus.Good;
|
||||
}
|
||||
}
|
||||
else if (quoteless.ToLowerInvariant() == "baddump")
|
||||
{
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
((Rom)item).ItemStatus = ItemStatus.BadDump;
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
((Disk)item).ItemStatus = ItemStatus.BadDump;
|
||||
}
|
||||
}
|
||||
else if (quoteless.ToLowerInvariant() == "nodump")
|
||||
{
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
((Rom)item).ItemStatus = ItemStatus.Nodump;
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
((Disk)item).ItemStatus = ItemStatus.Nodump;
|
||||
}
|
||||
}
|
||||
else if (quoteless.ToLowerInvariant() == "verified")
|
||||
{
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
((Rom)item).ItemStatus = ItemStatus.Verified;
|
||||
}
|
||||
else if (item.Type == ItemType.Disk)
|
||||
{
|
||||
((Disk)item).ItemStatus = ItemStatus.Verified;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "date":
|
||||
if (item.Type == ItemType.Rom)
|
||||
{
|
||||
// If we have quotes in the next item, assume only one item
|
||||
if (gc[i + 1].Contains("\""))
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "");
|
||||
}
|
||||
// Otherwise, we assume we need to read the next two items
|
||||
else
|
||||
{
|
||||
quoteless = gc[++i].Replace("\"", "") + " " + gc[++i].Replace("\"", "");
|
||||
}
|
||||
((Rom)item).Date = quoteless;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Now process and add the rom
|
||||
ParseAddHelper(item, clean, remUnicode);
|
||||
}
|
||||
|
||||
// If the line is anything but a rom or disk and we're in a block
|
||||
else if (Regex.IsMatch(line, Constants.ItemPatternCMP) && block)
|
||||
{
|
||||
GroupCollection gc = Regex.Match(line, Constants.ItemPatternCMP).Groups;
|
||||
|
||||
if (blockname != "header")
|
||||
{
|
||||
string itemval = gc[2].Value.Replace("\"", "");
|
||||
switch (gc[1].Value)
|
||||
{
|
||||
case "name":
|
||||
tempgamename = (itemval.ToLowerInvariant().EndsWith(".zip") ? itemval.Remove(itemval.Length - 4) : itemval);
|
||||
break;
|
||||
case "description":
|
||||
gamedesc = itemval;
|
||||
break;
|
||||
case "romof":
|
||||
romof = itemval;
|
||||
break;
|
||||
case "cloneof":
|
||||
cloneof = itemval;
|
||||
break;
|
||||
case "year":
|
||||
year = itemval;
|
||||
break;
|
||||
case "manufacturer":
|
||||
manufacturer = itemval;
|
||||
break;
|
||||
case "sampleof":
|
||||
sampleof = itemval;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string itemval = gc[2].Value.Replace("\"", "");
|
||||
|
||||
if (line.Trim().StartsWith("Name:"))
|
||||
{
|
||||
Name = (String.IsNullOrWhiteSpace(Name) ? line.Substring(6) : Name);
|
||||
superdat = superdat || itemval.Contains(" - SuperDAT");
|
||||
if (keep && superdat)
|
||||
{
|
||||
Type = (String.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (gc[1].Value)
|
||||
{
|
||||
case "name":
|
||||
case "Name:":
|
||||
Name = (String.IsNullOrWhiteSpace(Name) ? itemval : Name);
|
||||
superdat = superdat || itemval.Contains(" - SuperDAT");
|
||||
if (keep && superdat)
|
||||
{
|
||||
Type = (String.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type);
|
||||
}
|
||||
break;
|
||||
case "description":
|
||||
case "Description:":
|
||||
Description = (String.IsNullOrWhiteSpace(Description) ? itemval : Description);
|
||||
break;
|
||||
case "rootdir":
|
||||
RootDir = (String.IsNullOrWhiteSpace(RootDir) ? itemval : RootDir);
|
||||
break;
|
||||
case "category":
|
||||
Category = (String.IsNullOrWhiteSpace(Category) ? itemval : Category);
|
||||
break;
|
||||
case "version":
|
||||
case "Version:":
|
||||
Version = (String.IsNullOrWhiteSpace(Version) ? itemval : Version);
|
||||
break;
|
||||
case "date":
|
||||
case "Date:":
|
||||
Date = (String.IsNullOrWhiteSpace(Date) ? itemval : Date);
|
||||
break;
|
||||
case "author":
|
||||
case "Author:":
|
||||
Author = (String.IsNullOrWhiteSpace(Author) ? itemval : Author);
|
||||
break;
|
||||
case "email":
|
||||
Email = (String.IsNullOrWhiteSpace(Email) ? itemval : Email);
|
||||
break;
|
||||
case "homepage":
|
||||
case "Homepage:":
|
||||
Homepage = (String.IsNullOrWhiteSpace(Homepage) ? itemval : Homepage);
|
||||
break;
|
||||
case "url":
|
||||
Url = (String.IsNullOrWhiteSpace(Url) ? itemval : Url);
|
||||
break;
|
||||
case "comment":
|
||||
case "Comment:":
|
||||
Comment = (String.IsNullOrWhiteSpace(Comment) ? itemval : Comment);
|
||||
break;
|
||||
case "header":
|
||||
Header = (String.IsNullOrWhiteSpace(Header) ? itemval : Header);
|
||||
break;
|
||||
case "type":
|
||||
Type = (String.IsNullOrWhiteSpace(Type) ? itemval : Type);
|
||||
superdat = superdat || itemval.Contains("SuperDAT");
|
||||
break;
|
||||
case "forcemerging":
|
||||
if (ForceMerging == ForceMerging.None)
|
||||
{
|
||||
ForceMerging = Utilities.GetForceMerging(itemval);
|
||||
}
|
||||
break;
|
||||
case "forcezipping":
|
||||
if (ForcePacking == ForcePacking.None)
|
||||
{
|
||||
ForcePacking = Utilities.GetForcePacking(itemval);
|
||||
}
|
||||
break;
|
||||
case "forcepacking":
|
||||
if (ForcePacking == ForcePacking.None)
|
||||
{
|
||||
ForcePacking = Utilities.GetForcePacking(itemval);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we find an end bracket that's not associated with anything else, the block is done
|
||||
else if (Regex.IsMatch(line, Constants.EndPatternCMP) && block)
|
||||
{
|
||||
// If no items were found for this machine, add a Blank placeholder
|
||||
if (!containsItems)
|
||||
{
|
||||
Blank blank = new Blank()
|
||||
{
|
||||
MachineName = tempgamename,
|
||||
MachineDescription = gamedesc,
|
||||
CloneOf = cloneof,
|
||||
RomOf = romof,
|
||||
SampleOf = sampleof,
|
||||
Manufacturer = manufacturer,
|
||||
Year = year,
|
||||
|
||||
SystemID = sysid,
|
||||
SourceID = srcid,
|
||||
};
|
||||
|
||||
// Now process and add the rom
|
||||
ParseAddHelper(blank, clean, remUnicode);
|
||||
}
|
||||
|
||||
block = false; containsItems = false;
|
||||
blockname = ""; tempgamename = ""; gamedesc = ""; cloneof = "";
|
||||
romof = ""; sampleof = ""; year = ""; manufacturer = "";
|
||||
}
|
||||
}
|
||||
|
||||
sr.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a ClrMamePro DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="sysid">System ID for the DAT</param>
|
||||
/// <param name="srcid">Source ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="clean">True if game names are sanitized, false otherwise (default)</param>
|
||||
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
|
||||
public void ParseFileStripped(
|
||||
// Standard Dat parsing
|
||||
string filename,
|
||||
int sysid,
|
||||
int srcid,
|
||||
|
||||
// Miscellaneous
|
||||
bool keep,
|
||||
bool clean,
|
||||
bool remUnicode)
|
||||
{
|
||||
// Open a file reader
|
||||
Encoding enc = Utilities.GetEncoding(filename);
|
||||
StreamReader sr = new StreamReader(Utilities.TryOpenRead(filename), enc);
|
||||
|
||||
while (!sr.EndOfStream)
|
||||
{
|
||||
string line = sr.ReadLine();
|
||||
@@ -628,18 +73,20 @@ namespace SabreTools.Library.DatFiles
|
||||
if (Regex.IsMatch(line, Constants.HeaderPatternCMP))
|
||||
{
|
||||
GroupCollection gc = Regex.Match(line, Constants.HeaderPatternCMP).Groups;
|
||||
string normalizedValue = gc[1].Value.ToLowerInvariant();
|
||||
|
||||
// If we have a known header
|
||||
if (gc[1].Value == "clrmamepro"
|
||||
|| gc[1].Value == "romvault"
|
||||
|| gc[1].Value.ToLowerInvariant() == "doscenter")
|
||||
if (normalizedValue == "clrmamepro"
|
||||
|| normalizedValue == "romvault"
|
||||
|| normalizedValue == "doscenter")
|
||||
{
|
||||
ReadHeader(sr, keep);
|
||||
}
|
||||
// If we have a known set type
|
||||
else if (gc[1].Value == "set"
|
||||
|| gc[1].Value == "game"
|
||||
|| gc[1].Value == "machine")
|
||||
else if (normalizedValue == "set" // Used by the most ancient DATs
|
||||
|| normalizedValue == "game" // Used by most CMP DATs
|
||||
|| normalizedValue == "machine" // Possibly used by MAME CMP DATs
|
||||
|| normalizedValue == "resource") // Used by some other DATs to denote a BIOS set
|
||||
{
|
||||
ReadSet(sr, filename, sysid, srcid, keep, clean, remUnicode);
|
||||
}
|
||||
@@ -654,7 +101,6 @@ namespace SabreTools.Library.DatFiles
|
||||
/// </summary>
|
||||
/// <param name="reader">StreamReader to use to parse the header</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// TODO: Make sure this only is called if the block is "clrmamepro", "doscenter", "romcenter"
|
||||
private void ReadHeader(StreamReader reader, bool keep)
|
||||
{
|
||||
bool superdat = false;
|
||||
@@ -787,7 +233,6 @@ namespace SabreTools.Library.DatFiles
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="clean">True if game names are sanitized, false otherwise (default)</param>
|
||||
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
|
||||
/// TODO: Make sure this is only called if the block is "set", "game", "machine"
|
||||
private void ReadSet(
|
||||
StreamReader reader,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user