Skip to content

Commit d8bf92c

Browse files
committed
2.28.0 Initial
1 parent 40b26bd commit d8bf92c

21 files changed

Lines changed: 507 additions & 293 deletions

BenchmarkTests/BenchmarkTests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
12+
<PackageReference Include="BenchmarkDotNet" Version="0.15.0" />
1313
<PackageReference Include="Npgsql" Version="9.0.3" />
1414
<PackageReference Include="System.Text.Json" Version="9.0.5" />
1515
</ItemGroup>

NpgsqlRest/Defaults/DefaultCommentParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ internal static class DefaultCommentParser
4141
"content-type", // content-type is header key
4242
"content_type",
4343
];
44-
//private const string ContentTypeKey = "content-type";
44+
4545
private static readonly string[] requestHeadersModeKey = [
4646
"request_headers_mode",
4747
"request_headers",

NpgsqlRest/NpgsqlRest.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
<GenerateDocumentationFile>true</GenerateDocumentationFile>
3030
<PackageReadmeFile>README.MD</PackageReadmeFile>
3131
<DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
32-
<Version>2.27.0</Version>
33-
<AssemblyVersion>2.27.0</AssemblyVersion>
34-
<FileVersion>2.27.0</FileVersion>
35-
<PackageVersion>2.27.0</PackageVersion>
32+
<Version>2.28.0</Version>
33+
<AssemblyVersion>2.28.0</AssemblyVersion>
34+
<FileVersion>2.28.0</FileVersion>
35+
<PackageVersion>2.28.0</PackageVersion>
3636
</PropertyGroup>
3737

3838
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">

NpgsqlRest/UploadHandlers/CsvUploadHandler.cs

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,77 +8,66 @@ namespace NpgsqlRest.UploadHandlers;
88

99
public class CsvUploadHandler(NpgsqlRestUploadOptions options, ILogger? logger) : IUploadHandler
1010
{
11-
public bool RequiresTransaction => true;
12-
public string[] Parameters => _parameters;
13-
14-
private readonly string[] _parameters = [
15-
"included_mime_types",
16-
"excluded_mime_types",
17-
"check_file",
18-
"test_buffer_size",
19-
"non_printable_threshold",
20-
"delimiters",
21-
"has_fields_enclosed_in_quotes",
22-
"set_white_space_to_null",
23-
"row_command",
24-
];
11+
private const string CheckFileParam = "check_csv";
12+
private const string DelimitersParam = "delimiters";
13+
private const string HasFieldsEnclosedInQuotesParam = "has_fields_enclosed_in_quotes";
14+
private const string SetWhiteSpaceToNullParam = "set_white_space_to_null";
15+
private const string RowCommandParam = "row_command";
2516

2617
private string? _type = null;
2718

19+
public bool RequiresTransaction => true;
20+
public string[] Parameters => [
21+
UploadExtensions.IncludedMimeTypeParam, UploadExtensions.ExcludedMimeTypeParam,
22+
CheckFileParam, FileCheckExtensions.TestBufferSizeParam, FileCheckExtensions.NonPrintableThresholdParam, DelimitersParam,
23+
HasFieldsEnclosedInQuotesParam, SetWhiteSpaceToNullParam, RowCommandParam
24+
];
25+
2826
public IUploadHandler SetType(string type)
2927
{
3028
_type = type;
3129
return this;
3230
}
3331

34-
public async Task<object> UploadAsync(NpgsqlConnection connection, HttpContext context, Dictionary<string, string>? parameters)
32+
public async Task<string> UploadAsync(NpgsqlConnection connection, HttpContext context, Dictionary<string, string>? parameters)
3533
{
36-
string[]? includedMimeTypePatterns = options.DefaultUploadHandlerOptions.CsvUploadIncludedMimeTypePatterns;
37-
string[]? excludedMimeTypePatterns = options.DefaultUploadHandlerOptions.CsvUploadExcludedMimeTypePatterns;
34+
var (includedMimeTypePatterns, excludedMimeTypePatterns, _) = options.ParseSharedParameters(parameters);
3835

3936
bool checkFileStatus = options.DefaultUploadHandlerOptions.CsvUploadCheckFileStatus;
40-
int testBufferSize = options.DefaultUploadHandlerOptions.CsvUploadTestBufferSize;
41-
int nonPrintableThreshold = options.DefaultUploadHandlerOptions.CsvUploadNonPrintableThreshold;
37+
int testBufferSize = options.DefaultUploadHandlerOptions.TextTestBufferSize;
38+
int nonPrintableThreshold = options.DefaultUploadHandlerOptions.TextNonPrintableThreshold;
4239
string delimiters = options.DefaultUploadHandlerOptions.CsvUploadDelimiterChars;
4340
bool hasFieldsEnclosedInQuotes = options.DefaultUploadHandlerOptions.CsvUploadHasFieldsEnclosedInQuotes;
4441
bool setWhiteSpaceToNull = options.DefaultUploadHandlerOptions.CsvUploadSetWhiteSpaceToNull;
4542
string rowCommand = options.DefaultUploadHandlerOptions.CsvUploadRowCommand;
4643

4744
if (parameters is not null)
4845
{
49-
if (parameters.TryGetValue(_parameters[0], out var includedMimeTypeStr) && includedMimeTypeStr is not null)
50-
{
51-
includedMimeTypePatterns = includedMimeTypeStr.SplitParameter();
52-
}
53-
if (parameters.TryGetValue(_parameters[1], out var excludedMimeTypeStr) && excludedMimeTypeStr is not null)
54-
{
55-
excludedMimeTypePatterns = excludedMimeTypeStr.SplitParameter();
56-
}
57-
if (parameters.TryGetValue(_parameters[2], out var checkFileStatusStr) && bool.TryParse(checkFileStatusStr, out var checkFileStatusParsed))
46+
if (parameters.TryGetValue(CheckFileParam, out var checkFileStatusStr) && bool.TryParse(checkFileStatusStr, out var checkFileStatusParsed))
5847
{
5948
checkFileStatus = checkFileStatusParsed;
6049
}
61-
if (parameters.TryGetValue(_parameters[3], out var testBufferSizeStr) && int.TryParse(testBufferSizeStr, out var testBufferSizeParsed))
50+
if (parameters.TryGetValue(FileCheckExtensions.TestBufferSizeParam, out var testBufferSizeStr) && int.TryParse(testBufferSizeStr, out var testBufferSizeParsed))
6251
{
6352
testBufferSize = testBufferSizeParsed;
6453
}
65-
if (parameters.TryGetValue(_parameters[4], out var nonPrintableThresholdStr) && int.TryParse(nonPrintableThresholdStr, out var nonPrintableThresholdParsed))
54+
if (parameters.TryGetValue(FileCheckExtensions.NonPrintableThresholdParam, out var nonPrintableThresholdStr) && int.TryParse(nonPrintableThresholdStr, out var nonPrintableThresholdParsed))
6655
{
6756
nonPrintableThreshold = nonPrintableThresholdParsed;
6857
}
69-
if (parameters.TryGetValue(_parameters[5], out var delimitersStr) && delimitersStr is not null)
58+
if (parameters.TryGetValue(DelimitersParam, out var delimitersStr) && delimitersStr is not null)
7059
{
7160
delimiters = delimitersStr!;
7261
}
73-
if (parameters.TryGetValue(_parameters[6], out var hasFieldsEnclosedInQuotesStr) && bool.TryParse(hasFieldsEnclosedInQuotesStr, out var hasFieldsEnclosedInQuotesParsed))
62+
if (parameters.TryGetValue(HasFieldsEnclosedInQuotesParam, out var hasFieldsEnclosedInQuotesStr) && bool.TryParse(hasFieldsEnclosedInQuotesStr, out var hasFieldsEnclosedInQuotesParsed))
7463
{
7564
hasFieldsEnclosedInQuotes = hasFieldsEnclosedInQuotesParsed;
7665
}
77-
if (parameters.TryGetValue(_parameters[7], out var setWhiteSpaceToNullStr) && bool.TryParse(setWhiteSpaceToNullStr, out var setWhiteSpaceToNullParsed))
66+
if (parameters.TryGetValue(SetWhiteSpaceToNullParam, out var setWhiteSpaceToNullStr) && bool.TryParse(setWhiteSpaceToNullStr, out var setWhiteSpaceToNullParsed))
7867
{
7968
setWhiteSpaceToNull = setWhiteSpaceToNullParsed;
8069
}
81-
if (parameters.TryGetValue(_parameters[8], out var rowCommandStr) && rowCommandStr is not null)
70+
if (parameters.TryGetValue(RowCommandParam, out var rowCommandStr) && rowCommandStr is not null)
8271
{
8372
rowCommand = rowCommandStr;
8473
}

NpgsqlRest/UploadHandlers/DefaultUploadHandler.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Npgsql;
1+
using System.Text;
2+
using Microsoft.Extensions.Primitives;
3+
using Npgsql;
24

35
namespace NpgsqlRest.UploadHandlers;
46

@@ -17,13 +19,29 @@ public void OnError(NpgsqlConnection? connection, HttpContext context, Exception
1719
}
1820
}
1921

20-
public async Task<object> UploadAsync(NpgsqlConnection connection, HttpContext context, Dictionary<string, string>? parameters)
22+
public async Task<string> UploadAsync(NpgsqlConnection connection, HttpContext context, Dictionary<string, string>? parameters)
2123
{
22-
object[] results = new object[_handlers.Length];
24+
StringBuilder result = new(100);
2325
for (int i = 0; i < _handlers.Length; i++)
2426
{
25-
results[i] = await _handlers[i].UploadAsync(connection, context, parameters);
27+
var segment = await _handlers[i].UploadAsync(connection, context, parameters);
28+
if (i == 0 && _handlers.Length == 1)
29+
{
30+
result.Append(segment);
31+
}
32+
else if (i == 0 && _handlers.Length > 1)
33+
{
34+
result.Append(segment[..^1]).Append(',');
35+
}
36+
else if (i > 0 && i < _handlers.Length - 1)
37+
{
38+
result.Append(segment[1..^1]).Append(',');
39+
}
40+
else if (i > 0 && i == _handlers.Length - 1)
41+
{
42+
result.Append(segment[1..]);
43+
}
2644
}
27-
return results;
45+
return result.ToString();
2846
}
2947
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
namespace NpgsqlRest.UploadHandlers;
2+
3+
public enum UploadFileStatus { Empty, ProbablyBinary, NotImage, NoNewLines, Ok }
4+
5+
public static class FileCheckExtensions
6+
{
7+
private const string CheckTextParam = "check_text";
8+
private const string CheckImageParam = "check_image";
9+
10+
public const string TestBufferSizeParam = "test_buffer_size";
11+
public const string NonPrintableThresholdParam = "non_printable_threshold";
12+
13+
public static bool CheckMimeTypes(this string contentType, string[]? includedMimeTypePatterns, string[]? excludedMimeTypePatterns)
14+
{
15+
// File must match AT LEAST ONE included pattern
16+
if (includedMimeTypePatterns is not null && includedMimeTypePatterns.Length > 0)
17+
{
18+
bool matchesAny = false;
19+
for (int j = 0; j < includedMimeTypePatterns.Length; j++)
20+
{
21+
if (Parser.IsPatternMatch(contentType, includedMimeTypePatterns[j]))
22+
{
23+
matchesAny = true;
24+
break;
25+
}
26+
}
27+
28+
if (!matchesAny)
29+
{
30+
return false;
31+
}
32+
}
33+
34+
// File must NOT match ANY excluded patterns
35+
if (excludedMimeTypePatterns is not null)
36+
{
37+
for (int j = 0; j < excludedMimeTypePatterns.Length; j++)
38+
{
39+
if (Parser.IsPatternMatch(contentType, excludedMimeTypePatterns[j]))
40+
{
41+
return false;
42+
}
43+
}
44+
}
45+
46+
return true;
47+
}
48+
49+
public static async Task<UploadFileStatus> CheckFileStatus(
50+
this IFormFile formFile,
51+
int testBufferSize = 4096,
52+
int nonPrintableThreshold = 5,
53+
bool checkNewLines = true)
54+
{
55+
int length = formFile.Length < testBufferSize ? (int)formFile.Length : testBufferSize;
56+
57+
if (length == 0)
58+
{
59+
return UploadFileStatus.Empty;
60+
}
61+
62+
using var fileStream = formFile.OpenReadStream();
63+
byte[] buffer = new byte[length];
64+
int bytesRead = await fileStream.ReadAsync(buffer.AsMemory(0, length));
65+
66+
if (bytesRead == 0)
67+
{
68+
return UploadFileStatus.Empty;
69+
}
70+
71+
int nonPrintableCount = 0;
72+
int newLineCount = 0;
73+
74+
for (int i = 0; i < bytesRead; i++)
75+
{
76+
// Check for null byte - immediate binary indicator
77+
if (buffer[i] == 0)
78+
{
79+
return UploadFileStatus.ProbablyBinary;
80+
}
81+
82+
// Count newlines (LF character) if we're checking for them
83+
if (checkNewLines && buffer[i] == 10) // ASCII LF (Line Feed)
84+
{
85+
newLineCount++;
86+
}
87+
88+
// Count non-printable characters
89+
else if (buffer[i] < 32 && buffer[i] != 9 && buffer[i] != 13)
90+
{
91+
nonPrintableCount++;
92+
}
93+
}
94+
95+
// Check if binary based on non-printable characters
96+
if (nonPrintableCount > nonPrintableThreshold)
97+
{
98+
return UploadFileStatus.ProbablyBinary;
99+
}
100+
101+
// Only check for newlines if the parameter is true
102+
if (checkNewLines)
103+
{
104+
if (newLineCount > 0)
105+
{
106+
return UploadFileStatus.Ok;
107+
}
108+
return UploadFileStatus.NoNewLines;
109+
}
110+
else
111+
{
112+
// If we're not checking for newlines, consider it OK if it passes binary checks
113+
return UploadFileStatus.Ok;
114+
}
115+
}
116+
117+
public static async Task<bool> IsImage(this IFormFile file)
118+
{
119+
if (file == null || file.Length == 0)
120+
return false;
121+
122+
using var fileStream = file.OpenReadStream();
123+
var buffer = new byte[12];
124+
int bytesRead = await fileStream.ReadAsync(buffer);
125+
126+
return IsJpeg(buffer, bytesRead) || IsPng(buffer, bytesRead) || IsGif(buffer, bytesRead) ||
127+
IsBmp(buffer, bytesRead) || IsTiff(buffer, bytesRead) || IsWebp(buffer, bytesRead);
128+
}
129+
130+
private static bool IsJpeg(byte[] buffer, int bytesRead)
131+
{
132+
if (bytesRead < 3)
133+
{
134+
return false;
135+
}
136+
return buffer[0] == 0xFF && buffer[1] == 0xD8 && buffer[2] == 0xFF;
137+
}
138+
139+
private static bool IsPng(byte[] buffer, int bytesRead)
140+
{
141+
if (bytesRead < 4)
142+
{
143+
return false;
144+
}
145+
return buffer[0] == 0x89 && buffer[1] == 0x50 && buffer[2] == 0x4E && buffer[3] == 0x47;
146+
}
147+
148+
private static bool IsGif(byte[] buffer, int bytesRead)
149+
{
150+
if (bytesRead < 6)
151+
{
152+
return false;
153+
}
154+
return buffer[0] == 0x47 && buffer[1] == 0x49 && buffer[2] == 0x46 && buffer[3] == 0x38 &&
155+
(buffer[4] == 0x37 || buffer[4] == 0x39) && buffer[5] == 0x61; // GIF87a or GIF89a
156+
}
157+
158+
private static bool IsBmp(byte[] buffer, int bytesRead)
159+
{
160+
if (bytesRead < 2)
161+
{
162+
return false;
163+
}
164+
return buffer[0] == 0x42 && buffer[1] == 0x4D;
165+
}
166+
167+
private static bool IsTiff(byte[] buffer, int bytesRead)
168+
{
169+
if (bytesRead < 4)
170+
{
171+
return false;
172+
}
173+
return (buffer[0] == 0x49 && buffer[1] == 0x49 && buffer[2] == 0x2A && buffer[3] == 0x00) || // Little-endian
174+
(buffer[0] == 0x4D && buffer[1] == 0x4D && buffer[2] == 0x00 && buffer[3] == 0x2A); // Big-endian
175+
}
176+
177+
private static bool IsWebp(byte[] buffer, int bytesRead)
178+
{
179+
if (bytesRead < 12)
180+
{
181+
return false;
182+
}
183+
return buffer[0] == 0x52 && buffer[1] == 0x49 && buffer[2] == 0x46 && buffer[3] == 0x46 && // "RIFF"
184+
buffer[8] == 0x57 && buffer[9] == 0x45 && buffer[10] == 0x42 && buffer[11] == 0x50; // "WEBP"
185+
}
186+
}

0 commit comments

Comments
 (0)