Repository navigation
Expand file tree
/
Copy pathCsvProfileImportParser.cs
More file actions
182 lines (152 loc) · 5.8 KB
/
Copy pathCsvProfileImportParser.cs
File metadata and controls
182 lines (152 loc) · 5.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace NETworkManager.Profiles;
/// <summary>
/// Parses a CSV file into <see cref="ProfileImportCandidate" />s.
/// Expected format: <c>Name;Host</c> with an optional third <c>Description</c> column.
/// The delimiter (<c>;</c>, <c>,</c> or tab) is auto-detected and an optional header row is skipped.
/// </summary>
public static class CsvProfileImportParser
{
private static readonly char[] SupportedDelimiters = [';', ',', '\t'];
/// <summary>
/// Reads the given CSV file and returns one candidate per usable row.
/// </summary>
/// <param name="filePath">Path to the CSV file.</param>
/// <param name="fallbackDescription">
/// Description used when a row does not provide its own (third column). May be empty.
/// </param>
public static IReadOnlyList<ProfileImportCandidate> Parse(string filePath, string fallbackDescription = null)
{
var candidates = new List<ProfileImportCandidate>();
var lines = File.ReadAllLines(filePath);
var delimiter = DetectDelimiter(lines);
var headerChecked = false;
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line))
continue;
var fields = ParseLine(line, delimiter);
var name = fields.Count > 0 ? fields[0].Trim() : string.Empty;
var host = fields.Count > 1 ? fields[1].Trim() : string.Empty;
var description = fields.Count > 2 ? fields[2].Trim() : string.Empty;
// Skip an optional header row (only checked on the first non-empty line).
if (!headerChecked)
{
headerChecked = true;
if (IsHeaderRow(name, host))
continue;
}
// Ignore completely empty rows.
if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(host))
continue;
// Fall back to the host as name so we never create a nameless profile.
if (string.IsNullOrEmpty(name))
name = host;
candidates.Add(new ProfileImportCandidate(
name: name,
host: host,
description: !string.IsNullOrEmpty(description) ? description : fallbackDescription,
importSource: ProfileImportSource.Csv,
importSourceId: BuildImportSourceId(name, host)));
}
return candidates;
}
/// <summary>
/// Builds a stable duplicate-detection key from the import source and a hash of name + host.
/// </summary>
private static string BuildImportSourceId(string name, string host)
{
var raw = $"Csv|{name.Trim().ToLowerInvariant()}|{host.Trim().ToLowerInvariant()}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
return Convert.ToHexString(hash).ToLowerInvariant();
}
/// <summary>
/// Picks the delimiter that occurs most often on the first non-empty line.
/// </summary>
private static char DetectDelimiter(IReadOnlyList<string> lines)
{
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line))
continue;
var bestDelimiter = SupportedDelimiters[0];
var bestCount = 0;
foreach (var delimiter in SupportedDelimiters)
{
var count = 0;
foreach (var c in line)
if (c == delimiter)
count++;
if (count <= bestCount)
continue;
bestCount = count;
bestDelimiter = delimiter;
}
return bestDelimiter;
}
return SupportedDelimiters[0];
}
/// <summary>
/// Detects whether the first row is a header (e.g. <c>Name;Host</c>).
/// </summary>
private static bool IsHeaderRow(string firstField, string secondField)
{
return string.Equals(firstField, "Name", StringComparison.OrdinalIgnoreCase) &&
(string.Equals(secondField, "Host", StringComparison.OrdinalIgnoreCase) ||
string.Equals(secondField, "Host/IP", StringComparison.OrdinalIgnoreCase) ||
string.Equals(secondField, "Hostname", StringComparison.OrdinalIgnoreCase) ||
string.Equals(secondField, "IP", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Splits a single CSV line by the given delimiter, honoring double-quoted fields.
/// </summary>
private static List<string> ParseLine(string line, char delimiter)
{
var fields = new List<string>();
var builder = new StringBuilder();
var inQuotes = false;
for (var i = 0; i < line.Length; i++)
{
var c = line[i];
if (inQuotes)
{
if (c == '"')
{
// Escaped quote ("") inside a quoted field.
if (i + 1 < line.Length && line[i + 1] == '"')
{
builder.Append('"');
i++;
}
else
{
inQuotes = false;
}
}
else
{
builder.Append(c);
}
}
else if (c == '"')
{
inQuotes = true;
}
else if (c == delimiter)
{
fields.Add(builder.ToString());
builder.Clear();
}
else
{
builder.Append(c);
}
}
fields.Add(builder.ToString());
return fields;
}
}