WinGui: Add an RFC4180 compliant CSV parser helper method to fix chapter imports with strings that include ,. Fixes #7485

This commit is contained in:
sr55 2025-12-13 14:47:19 +00:00
parent 8e27c6f3d8
commit 19a618d2c4
No known key found for this signature in database
GPG Key ID: ECE911849A3E21A5
2 changed files with 130 additions and 39 deletions

View File

@ -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' : ',';
}
}
}

View File

@ -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)