mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 16:32:20 -04:00
Add custom decoder support for importers (#1146)
This commit is contained in:
committed by
Leendert de Borst
parent
ad3c0323b9
commit
2fccb162e6
@@ -68,7 +68,6 @@
|
||||
<EmbeddedResource Include="TestData\Exports\strongbox.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\keepass.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\keepass_special_chars.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\keepass_escaped_quotes.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\keepassxc.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\1password_8.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\protonpass.csv" />
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"Account","Login Name","Password","Web Site","Comments"
|
||||
"Sample Entry","User Name","Password","https://keepass.info/","Notes"
|
||||
"Entry with backslash quotes","testuser","mypass123","https://example.com","This note has \"escaped quotes\" and other stuff"
|
||||
|
Can't render this file because it contains an unexpected character in line 3 and column 92.
|
@@ -1,4 +1,4 @@
|
||||
"Account","Login Name","Password","Web Site","Comments"
|
||||
"Sample Entry","User Name","Password","https://keepass.info/","Notes"
|
||||
"Sample Entry #2","Michael321","12345","https://keepass.info/help/kb/testform.html",""
|
||||
"Entry with notes special chars","","DVfIsb4TGkL7oKCwyiet","","Note \"with double quotes\"'as'd as/d/asd/ z's'sd a8e89A)_@()@'\":ÄS\"d';asd;á'sd"
|
||||
"Entry with \"notes\" special chars","","DVfIsb4TGkL7oKCwyiet","","Note \"with quotes\"'as'd as/d/asd/ z's'sd a8e89A)_@()@'"":ÄS""d';asd;á'sd"
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 70.
|
@@ -336,14 +336,14 @@ public class ImportExportTests
|
||||
Assert.That(importedCredentials, Has.Count.EqualTo(3));
|
||||
|
||||
// Test the entry with special characters and double quotes
|
||||
var specialEntry = importedCredentials.First(c => c.ServiceName == "Entry with notes special chars");
|
||||
var specialEntry = importedCredentials.First(c => c.ServiceName?.StartsWith("Entry with") ?? false);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(specialEntry.ServiceName, Is.EqualTo("Entry with notes special chars"));
|
||||
Assert.That(specialEntry.ServiceName, Is.EqualTo("Entry with \"notes\" special chars"));
|
||||
Assert.That(specialEntry.ServiceUrl, Is.Empty);
|
||||
Assert.That(specialEntry.Username, Is.Empty);
|
||||
Assert.That(specialEntry.Password, Is.EqualTo("DVfIsb4TGkL7oKCwyiet"));
|
||||
Assert.That(specialEntry.Notes, Does.Contain("\"Note with double quotes\""));
|
||||
Assert.That(specialEntry.Notes, Does.Contain("\"with quotes\""));
|
||||
Assert.That(specialEntry.Notes, Does.Contain("as'd as/d/asd/ z"));
|
||||
Assert.That(specialEntry.Notes, Does.Contain("asd;á'sd"));
|
||||
});
|
||||
@@ -360,41 +360,6 @@ public class ImportExportTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case for importing credentials from KeePass CSV with backslash-escaped quotes.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task ImportCredentialsFromKeePassCsvWithEscapedQuotes()
|
||||
{
|
||||
// Arrange
|
||||
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.keepass_escaped_quotes.csv");
|
||||
|
||||
// Act
|
||||
var importedCredentials = await KeePassImporter.ImportFromCsvAsync(fileContent);
|
||||
|
||||
// Assert
|
||||
Assert.That(importedCredentials, Has.Count.EqualTo(2));
|
||||
|
||||
// Test the entry with escaped quotes
|
||||
var escapedQuotesEntry = importedCredentials.First(c => c.ServiceName == "Entry with backslash quotes");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(escapedQuotesEntry.ServiceName, Is.EqualTo("Entry with backslash quotes"));
|
||||
Assert.That(escapedQuotesEntry.ServiceUrl, Is.EqualTo("https://example.com"));
|
||||
Assert.That(escapedQuotesEntry.Username, Is.EqualTo("testuser"));
|
||||
Assert.That(escapedQuotesEntry.Password, Is.EqualTo("mypass123"));
|
||||
|
||||
// The CSV decoding should have converted \" to " properly
|
||||
Assert.That(escapedQuotesEntry.Notes, Does.Contain("This note has"));
|
||||
Assert.That(escapedQuotesEntry.Notes, Does.Contain("\"escaped quotes\""));
|
||||
Assert.That(escapedQuotesEntry.Notes, Does.Contain("and other stuff"));
|
||||
|
||||
// Verify that the backslashes before quotes have been properly decoded
|
||||
Assert.That(escapedQuotesEntry.Notes, Does.Not.Contain("\\\""));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case for importing credentials from KeePassXC CSV and ensuring all values are present.
|
||||
/// </summary>
|
||||
|
||||
@@ -56,8 +56,9 @@ public static class BaseImporter
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The CSV record type.</typeparam>
|
||||
/// <param name="fileContent">The CSV file content.</param>
|
||||
/// <param name="customDecoder">Optional custom field decoder function.</param>
|
||||
/// <returns>A list of parsed CSV records.</returns>
|
||||
public static async Task<List<T>> ImportCsvDataAsync<T>(string fileContent)
|
||||
public static async Task<List<T>> ImportCsvDataAsync<T>(string fileContent, Func<string, string>? customDecoder = null)
|
||||
{
|
||||
using var reader = new StringReader(fileContent);
|
||||
using var csv = new CsvReader(reader, CreateCsvConfiguration());
|
||||
@@ -72,7 +73,7 @@ public static class BaseImporter
|
||||
lineNumber++;
|
||||
|
||||
// Process CSV field decoding for escaped quotes and other special characters
|
||||
DecodeFields(record);
|
||||
DecodeFields(record, customDecoder);
|
||||
|
||||
records.Add(record);
|
||||
}
|
||||
@@ -96,7 +97,8 @@ public static class BaseImporter
|
||||
/// Specifically handles CSV-encoded double quotes and other escape sequences.
|
||||
/// </summary>
|
||||
/// <param name="record">The CSV record to process.</param>
|
||||
private static void DecodeFields<T>(T record)
|
||||
/// <param name="customDecoder">Optional custom decoder function for importer-specific decoding.</param>
|
||||
private static void DecodeFields<T>(T record, Func<string, string>? customDecoder = null)
|
||||
{
|
||||
if (record == null) return;
|
||||
|
||||
@@ -110,7 +112,7 @@ public static class BaseImporter
|
||||
var value = property.GetValue(record) as string;
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var decodedValue = DecodeCsvField(value);
|
||||
var decodedValue = customDecoder?.Invoke(value) ?? DecodeCsvField(value);
|
||||
property.SetValue(record, decodedValue);
|
||||
}
|
||||
}
|
||||
@@ -118,7 +120,7 @@ public static class BaseImporter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a CSV field value by handling escaped quotes and other CSV encoding.
|
||||
/// Decodes a CSV field value by handling standard CSV escaped quotes.
|
||||
/// </summary>
|
||||
/// <param name="value">The CSV field value.</param>
|
||||
/// <returns>The decoded value.</returns>
|
||||
@@ -127,22 +129,11 @@ public static class BaseImporter
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return value;
|
||||
|
||||
// Handle CSV-escaped double quotes: "" becomes "
|
||||
// But be careful not to affect other legitimate escape sequences
|
||||
var decoded = value;
|
||||
|
||||
// Replace CSV-style escaped quotes (two consecutive quotes) with single quotes
|
||||
// Handle standard CSV-style escaped quotes (two consecutive quotes) -> single quote
|
||||
decoded = decoded.Replace("\"\"", "\"");
|
||||
|
||||
// Handle common backslash escape sequences that might come from KeePass or other sources
|
||||
// Only handle specific known escape sequences to avoid breaking valid backslashes
|
||||
decoded = decoded.Replace("\\\"", "\""); // Escaped quote
|
||||
decoded = decoded.Replace("\\n", "\n"); // Escaped newline
|
||||
decoded = decoded.Replace("\\r", "\r"); // Escaped carriage return
|
||||
decoded = decoded.Replace("\\t", "\t"); // Escaped tab
|
||||
|
||||
// Don't replace all backslashes as they might be legitimate (e.g., in file paths)
|
||||
|
||||
return decoded;
|
||||
}
|
||||
/// <summary>
|
||||
|
||||
@@ -12,12 +12,50 @@ using AliasVault.ImportExport.Models.Imports;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// Imports credentials from KeePass.
|
||||
/// </summary>
|
||||
public static class KeePassImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Decodes KeePass 1.x specific field encoding.
|
||||
/// KeePass 1.x rules: Quotes (") in strings are encoded as \" (two characters).
|
||||
/// Backslashes (\) are encoded as \\ (two characters).
|
||||
/// </summary>
|
||||
/// <param name="value">The field value to decode.</param>
|
||||
/// <returns>The decoded value.</returns>
|
||||
private static string DecodeKeePassField(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return value;
|
||||
|
||||
var decoded = value;
|
||||
|
||||
// Handle standard CSV-style escaped quotes first (two consecutive quotes) -> single quote
|
||||
decoded = decoded.Replace("\"\"", "\"");
|
||||
|
||||
// Handle KeePass 1.x specific encoding rules
|
||||
// Backslashes (\) are encoded as \\ -> \
|
||||
decoded = decoded.Replace("\\\\", "\\");
|
||||
|
||||
// Quotes (") in strings are encoded as \" -> "
|
||||
decoded = decoded.Replace("\\\"", "\"");
|
||||
|
||||
// Special handling for the case where the CSV parser has already partially processed
|
||||
// the escaped quotes, leaving single backslashes that should be quotes
|
||||
// This handles the case where \with should become "with
|
||||
if (decoded.Contains("\\"))
|
||||
{
|
||||
// Look for standalone backslashes that should be quotes
|
||||
// This is a fallback for malformed or partially-parsed KeePass data
|
||||
decoded = Regex.Replace(decoded, @"\\(?![\\\""])", "\"");
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports KeePass CSV file and converts contents to list of ImportedCredential model objects.
|
||||
/// </summary>
|
||||
@@ -25,7 +63,8 @@ public static class KeePassImporter
|
||||
/// <returns>The imported list of ImportedCredential objects.</returns>
|
||||
public static async Task<List<ImportedCredential>> ImportFromCsvAsync(string fileContent)
|
||||
{
|
||||
var records = await BaseImporter.ImportCsvDataAsync<KeePassCsvRecord>(fileContent);
|
||||
// Use KeePass-specific field decoder for proper handling of KeePass 1.x encoding
|
||||
var records = await BaseImporter.ImportCsvDataAsync<KeePassCsvRecord>(fileContent, DecodeKeePassField);
|
||||
|
||||
var credentials = new List<ImportedCredential>();
|
||||
foreach (var record in records)
|
||||
|
||||
Reference in New Issue
Block a user