Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/NuGetLicense/CommandLineOptionsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ public IOutputFormatter GetOutputFormatter(OutputType outputType, bool returnErr
OutputType.JsonPretty => new Output.Json.JsonOutputFormatter(true, returnErrorsOnly, !includeIgnoredPackages),
OutputType.Table => new Output.Table.TableOutputFormatter(returnErrorsOnly, !includeIgnoredPackages),
OutputType.Markdown => new Output.Table.TableOutputFormatter(returnErrorsOnly, !includeIgnoredPackages, printMarkdown: true),
OutputType.Csv => new Output.Csv.CsvOutputFormatter(returnErrorsOnly, !includeIgnoredPackages),
_ => throw new ArgumentOutOfRangeException($"{outputType} not supported")
};
}
Expand Down
90 changes: 90 additions & 0 deletions src/NuGetLicense/Output/Csv/CsvOutputFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Licensed to the projects contributors.
// The license conditions are provided in the LICENSE file located in the project root

using System.Globalization;
using NuGetLicense.LicenseValidator;

namespace NuGetLicense.Output.Csv
{
/// <summary>
/// Outputs license validation results in CSV format.
/// </summary>
public class CsvOutputFormatter : IOutputFormatter
{
private readonly bool _printErrorsOnly;
private readonly bool _skipIgnoredPackages;

public CsvOutputFormatter(bool printErrorsOnly, bool skipIgnoredPackages)
{
_printErrorsOnly = printErrorsOnly;
_skipIgnoredPackages = skipIgnoredPackages;
}

public async Task Write(Stream stream, IList<LicenseValidationResult> results)
{
if (_printErrorsOnly)
{
results = results.Where(r => r.ValidationErrors.Any()).ToList();
}
else if (_skipIgnoredPackages)
{
results = results.Where(r => r.LicenseInformationOrigin != LicenseInformationOrigin.Ignored).ToList();
}

using var writer = new StreamWriter(stream, leaveOpen: true);

// Write header
await writer.WriteLineAsync("Package,Version,License Information Origin,License Expression,License Url,Copyright,Authors,Description,Summary,Error,Error Context");

// Write rows
foreach (var result in results)
{
string errors = result.ValidationErrors.Any()
? string.Join("; ", result.ValidationErrors.Select(e => EscapeCsvField(e.Error)))
: "";
string errorContexts = result.ValidationErrors.Any()
? string.Join("; ", result.ValidationErrors.Select(e => EscapeCsvField(e.Context)))
: "";

var line = string.Join(",",
EscapeCsvField(result.PackageId),
EscapeCsvField(result.PackageVersion.ToString()),
EscapeCsvField(result.LicenseInformationOrigin.ToString()),
EscapeCsvField(result.License),
EscapeCsvField(result.LicenseUrl),
EscapeCsvField(result.Copyright),
EscapeCsvField(result.Authors),
EscapeCsvField(result.Description),
EscapeCsvField(result.Summary),
EscapeCsvField(errors),
EscapeCsvField(errorContexts)
Comment on lines +42 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid double-escaping Error and Error Context values.

Line 42–47 escapes each error item, and Line 59–60 escapes the joined field again. This can produce incorrect CSV cell content (extra quotes).

Proposed fix
-                string errors = result.ValidationErrors.Any() 
-                    ? string.Join("; ", result.ValidationErrors.Select(e => EscapeCsvField(e.Error))) 
-                    : "";
-                string errorContexts = result.ValidationErrors.Any() 
-                    ? string.Join("; ", result.ValidationErrors.Select(e => EscapeCsvField(e.Context))) 
-                    : "";
+                string errors = result.ValidationErrors.Any()
+                    ? string.Join("; ", result.ValidationErrors.Select(e => e.Error))
+                    : string.Empty;
+                string errorContexts = result.ValidationErrors.Any()
+                    ? string.Join("; ", result.ValidationErrors.Select(e => e.Context))
+                    : string.Empty;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
string errors = result.ValidationErrors.Any()
? string.Join("; ", result.ValidationErrors.Select(e => EscapeCsvField(e.Error)))
: "";
string errorContexts = result.ValidationErrors.Any()
? string.Join("; ", result.ValidationErrors.Select(e => EscapeCsvField(e.Context)))
: "";
var line = string.Join(",",
EscapeCsvField(result.PackageId),
EscapeCsvField(result.PackageVersion.ToString()),
EscapeCsvField(result.LicenseInformationOrigin.ToString()),
EscapeCsvField(result.License),
EscapeCsvField(result.LicenseUrl),
EscapeCsvField(result.Copyright),
EscapeCsvField(result.Authors),
EscapeCsvField(result.Description),
EscapeCsvField(result.Summary),
EscapeCsvField(errors),
EscapeCsvField(errorContexts)
string errors = result.ValidationErrors.Any()
? string.Join("; ", result.ValidationErrors.Select(e => e.Error))
: string.Empty;
string errorContexts = result.ValidationErrors.Any()
? string.Join("; ", result.ValidationErrors.Select(e => e.Context))
: string.Empty;
var line = string.Join(",",
EscapeCsvField(result.PackageId),
EscapeCsvField(result.PackageVersion.ToString()),
EscapeCsvField(result.LicenseInformationOrigin.ToString()),
EscapeCsvField(result.License),
EscapeCsvField(result.LicenseUrl),
EscapeCsvField(result.Copyright),
EscapeCsvField(result.Authors),
EscapeCsvField(result.Description),
EscapeCsvField(result.Summary),
EscapeCsvField(errors),
EscapeCsvField(errorContexts)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NuGetLicense/Output/Csv/CsvOutputFormatter.cs` around lines 42 - 60, The
ValidationErrors are being escaped twice: each item is passed through
EscapeCsvField in the Select and then the joined string (errors / errorContexts)
is passed through EscapeCsvField again when building the CSV line; change this
by removing the per-item escaping in the Selects that build errors and
errorContexts so they produce plain strings (e.g. string.Join("; ",
result.ValidationErrors.Select(e => e.Error))) and keep the single
EscapeCsvField call when adding errors and errorContexts to the CSV line; update
references in CsvOutputFormatter.cs to only call EscapeCsvField on the final
errors and errorContexts variables.

);

await writer.WriteLineAsync(line);
}

await writer.FlushAsync();
}

/// <summary>
/// Escapes a field for CSV output.
/// Handles fields containing commas, quotes, or newlines.
/// </summary>
private static string EscapeCsvField(string? field)
{
if (string.IsNullOrEmpty(field))
{
return "";
}

// If the field contains comma, quote, or newline, wrap it in quotes
if (field.Contains(',') || field.Contains('"') || field.Contains('\n') || field.Contains('\r'))
{
// Replace double quotes with two double quotes
return "\"" + field.Replace("\"", "\"\"") + "\"";
}

return field;
}
}
}
2 changes: 1 addition & 1 deletion src/NuGetLicense/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class Program : ICommandLineOptions
[Option("-d|--license-information-download-location", Description = "Specifies a folder where the application will download all licenses provided via license URLs.")]
public string? DownloadLicenseInformation { get; set; }

[Option("-o|--output", Description = "Specifies the output format. Valid values are Table, Markdown, Json, or JsonPretty (default: Table).")]
[Option("-o|--output", Description = "Specifies the output format. Valid values are Table, Markdown, Json, JsonPretty, or Csv (default: Table).")]
public OutputType OutputType { get; set; } = OutputType.Table;

[Option("-err|--error-only", Description = "When set, only validation errors are returned as result. Otherwise, all validation results are always returned.")]
Expand Down
5 changes: 3 additions & 2 deletions src/NuGetUtility/OutputType.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the projects contributors.
// Licensed to the projects contributors.
// The license conditions are provided in the LICENSE file located in the project root

namespace NuGetUtility
Expand All @@ -8,6 +8,7 @@ public enum OutputType
Table,
Json,
JsonPretty,
Markdown
Markdown,
Csv
}
}