mirror of https://github.com/HandBrake/HandBrake
WinGui: Add an RFC4180 compliant CSV parser helper method to fix chapter imports with strings that include ,. Fixes #7485
This commit is contained in:
parent
8e27c6f3d8
commit
19a618d2c4
|
|
@ -7,10 +7,15 @@
|
|||
// </summary>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace HandBrake.App.Core.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility functions for writing CSV files
|
||||
/// Utility functions for working with CSV files
|
||||
/// </summary>
|
||||
public sealed class CsvHelper
|
||||
{
|
||||
|
|
@ -26,12 +31,121 @@ namespace HandBrake.App.Core.Utilities
|
|||
public static string Escape(string value)
|
||||
{
|
||||
if (value.Contains(QUOTE))
|
||||
{
|
||||
value = value.Replace(QUOTE, ESCAPED_QUOTE);
|
||||
}
|
||||
|
||||
if (value.IndexOfAny(CHARACTERS_THAT_MUST_BE_QUOTED) > -1)
|
||||
{
|
||||
value = QUOTE + value + QUOTE;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a CSV or TSV file and parses it according to RFC 4180.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the csv or tsv file. </param>
|
||||
/// <returns>Parsed rows where each row is an array of column values.</returns>
|
||||
public static IList<string[]> ParseFile(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
throw new ArgumentException("File path must be provided.", nameof(filePath));
|
||||
}
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException("The specified file was not found.", filePath);
|
||||
}
|
||||
|
||||
var rows = new List<string[]>();
|
||||
var delimiter = DetermineDelimiter(filePath);
|
||||
|
||||
using (var reader = new StreamReader(filePath, Encoding.UTF8, true))
|
||||
{
|
||||
var fieldBuilder = new StringBuilder();
|
||||
var currentRow = new List<string>();
|
||||
var inQuotes = false;
|
||||
|
||||
while (reader.Peek() != -1)
|
||||
{
|
||||
var character = (char)reader.Read();
|
||||
|
||||
if (inQuotes)
|
||||
{
|
||||
if (character == '"')
|
||||
{
|
||||
if (reader.Peek() == '"')
|
||||
{
|
||||
reader.Read();
|
||||
fieldBuilder.Append('"');
|
||||
}
|
||||
else
|
||||
{
|
||||
inQuotes = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldBuilder.Append(character);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '"')
|
||||
{
|
||||
if (fieldBuilder.Length > 0)
|
||||
{
|
||||
throw new FormatException("There was a malformed quoted field detected.");
|
||||
}
|
||||
|
||||
inQuotes = true;
|
||||
}
|
||||
else if (character == delimiter)
|
||||
{
|
||||
currentRow.Add(fieldBuilder.ToString());
|
||||
fieldBuilder.Clear();
|
||||
}
|
||||
else if (character == '\r' || character == '\n')
|
||||
{
|
||||
if (character == '\r' && reader.Peek() == '\n')
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
currentRow.Add(fieldBuilder.ToString());
|
||||
fieldBuilder.Clear();
|
||||
rows.Add(currentRow.ToArray());
|
||||
currentRow.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldBuilder.Append(character);
|
||||
}
|
||||
}
|
||||
|
||||
if (inQuotes)
|
||||
{
|
||||
throw new FormatException("There was a unterminated quoted field detected");
|
||||
}
|
||||
|
||||
if (fieldBuilder.Length > 0 || currentRow.Count > 0)
|
||||
{
|
||||
currentRow.Add(fieldBuilder.ToString());
|
||||
rows.Add(currentRow.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static char DetermineDelimiter(string filePath)
|
||||
{
|
||||
var extension = Path.GetExtension(filePath);
|
||||
return extension.Equals(".tsv", StringComparison.OrdinalIgnoreCase) ? '\t' : ',';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ namespace HandBrakeWPF.Utilities.Input
|
|||
using System.IO;
|
||||
|
||||
using HandBrake.App.Core.Exceptions;
|
||||
using HandBrake.App.Core.Utilities;
|
||||
|
||||
using HandBrakeWPF.Properties;
|
||||
|
||||
|
|
@ -37,47 +38,23 @@ namespace HandBrakeWPF.Utilities.Input
|
|||
int lineNumber = 0;
|
||||
try
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(filename))
|
||||
IList<string[]> fileConents = CsvHelper.ParseFile(filename);
|
||||
foreach (string[] row in fileConents)
|
||||
{
|
||||
// Try guess the delimiter.
|
||||
string contents = reader.ReadToEnd();
|
||||
reader.DiscardBufferedData();
|
||||
reader.BaseStream.Seek(0, SeekOrigin.Begin);
|
||||
bool tabDelimited = contents.Split('\t').Length > contents.Split(',').Length;
|
||||
|
||||
// Parse each line.
|
||||
while (reader.Peek() >= 0)
|
||||
if (row.Length < 2)
|
||||
{
|
||||
lineNumber = lineNumber + 1;
|
||||
string line = reader.ReadLine();
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
string[] splitContents = tabDelimited ? line.Split('\t') : line.Split(',');
|
||||
|
||||
if (splitContents.Length < 2)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
string.Format(
|
||||
Resources
|
||||
.ChaptersViewModel_UnableToImportChaptersLineDoesNotHaveAtLeastTwoColumns,
|
||||
lineNumber));
|
||||
}
|
||||
|
||||
if (!int.TryParse(splitContents[0], out var chapterNumber))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
string.Format(
|
||||
Resources
|
||||
.ChaptersViewModel_UnableToImportChaptersFirstColumnMustContainOnlyIntegerNumber,
|
||||
lineNumber));
|
||||
}
|
||||
|
||||
string chapterName = splitContents[1].Trim();
|
||||
|
||||
// Store the chapter name at the correct index
|
||||
importedChapters[chapterNumber] = new Tuple<string, TimeSpan>(chapterName, TimeSpan.Zero);
|
||||
}
|
||||
throw new InvalidDataException(string.Format(Resources.ChaptersViewModel_UnableToImportChaptersLineDoesNotHaveAtLeastTwoColumns, lineNumber));
|
||||
}
|
||||
|
||||
if (!int.TryParse(row[0], out var chapterNumber))
|
||||
{
|
||||
throw new InvalidDataException(string.Format(Resources.ChaptersViewModel_UnableToImportChaptersFirstColumnMustContainOnlyIntegerNumber, lineNumber));
|
||||
}
|
||||
|
||||
string chapterName = row[1].Trim();
|
||||
|
||||
// Store the chapter name at the correct index
|
||||
importedChapters[chapterNumber] = new Tuple<string, TimeSpan>(chapterName, TimeSpan.Zero);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
|||
Loading…
Reference in New Issue