Skip to content

Commit 7019e8e

Browse files
committed
Update to v2.0.4
1 parent f700d76 commit 7019e8e

4 files changed

Lines changed: 206 additions & 42 deletions

File tree

.github/workflows/dotnet.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ jobs:
5757
- name: Restore dependencies
5858
run: dotnet restore IniFile.slnx
5959

60+
- name: Validate package version tag
61+
if: startsWith(github.ref, 'refs/tags/')
62+
shell: pwsh
63+
run: |
64+
[xml]$project = Get-Content src/IniFile/IniFile.csproj
65+
$packageVersion = [string]$project.Project.PropertyGroup.Version
66+
$tagVersion = $env:GITHUB_REF_NAME -replace '^v', ''
67+
if ($packageVersion -ne $tagVersion) {
68+
throw "Package version '$packageVersion' does not match tag '$env:GITHUB_REF_NAME'."
69+
}
70+
6071
- name: Pack
6172
run: dotnet pack src/IniFile/IniFile.csproj --no-restore --configuration Release --output ./nupkg
6273

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,25 +140,25 @@ Creates a new instance bound to the given file path. The path is resolved to an
140140
```csharp
141141
public bool Write(string key, string? value, string? section = null)
142142
```
143-
Writes a string value. Returns `true` on success. Creates the file, section, and key if they don't exist.
143+
Writes a string value. Returns `true` on success. Creates the file, section, and key if they don't exist. When `section` is omitted or `null`, the wrapper uses an empty section name, which Windows serializes as `[]`.
144144

145145
#### 📖 `ReadString`
146146
```csharp
147-
public string ReadString(string key, string? section = null, string defaultValue = "", int bufferSize = 1024)
147+
public string ReadString(string? key, string? section = null, string defaultValue = "", int bufferSize = 1024)
148148
```
149-
Returns the value for the key, or `defaultValue` if not found.
149+
Returns the value for the key, or `defaultValue` if not found. When `section` is omitted or `null`, key reads use an empty section name, which Windows serializes as `[]`. Passing `null` for `key` preserves the native enumeration behavior: key names for a section, or section names when both `key` and `section` are `null`.
150150

151151
#### 🔢 `ReadInt`
152152
```csharp
153153
public int ReadInt(string key, string? section = null, int defaultValue = -1)
154154
```
155-
Returns the integer value for the key, or `defaultValue` if not found or not a valid integer.
155+
Returns the integer value for the key, or `defaultValue` if the key is not found. Malformed numeric values follow the native `GetPrivateProfileInt` parsing behavior and may return `0` instead of `defaultValue`. When `section` is omitted or `null`, the wrapper uses an empty section name, which Windows serializes as `[]`.
156156

157157
#### `ReadBool`
158158
```csharp
159159
public bool ReadBool(string key, string? section = null, bool defaultValue = false)
160160
```
161-
Returns `true` for `"true"`, `"1"`, `"yes"`; `false` for `"false"`, `"0"`, `"no"` (case-insensitive). Returns `defaultValue` for anything else.
161+
Returns `true` for `"true"`, `"1"`, `"yes"`; `false` for `"false"`, `"0"`, `"no"` (case-insensitive). Returns `defaultValue` for anything else. When `section` is omitted or `null`, the wrapper uses an empty section name, which Windows serializes as `[]`.
162162

163163
#### 📂 `GetAllSections`
164164
```csharp
@@ -176,19 +176,19 @@ Returns an array of `"key=value"` strings for every entry in the given section.
176176
```csharp
177177
public bool DeleteKey(string key, string? section = null)
178178
```
179-
Removes a key and its value. Returns `true` on success.
179+
Removes a key and its value. Returns `true` on success. When `section` is omitted or `null`, the wrapper uses an empty section name, which Windows serializes as `[]`.
180180

181181
#### 🧹 `DeleteSection`
182182
```csharp
183183
public bool DeleteSection(string? section = null)
184184
```
185-
Removes an entire section. Returns `true` on success.
185+
Removes an entire section. Returns `true` on success. When `section` is omitted or `null`, the wrapper deletes the empty `[]` section.
186186

187187
#### 🔍 `KeyExists`
188188
```csharp
189189
public bool KeyExists(string key, string? section = null)
190190
```
191-
Returns `true` if the key exists in the section (including keys with empty values).
191+
Returns `true` if the key exists in the section (including keys with empty values). When `section` is omitted or `null`, the wrapper uses an empty section name, which Windows serializes as `[]`.
192192

193193
---
194194

src/IniFile/IniFile.cs

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ public sealed partial class IniFile
3030
/// <summary>The fully-qualified path to the INI file.</summary>
3131
private readonly string _filePath;
3232

33+
private static string NormalizeSection(string? section) => section ?? string.Empty;
34+
35+
private static void ValidateBufferSize(int bufferSize, int minimumSize = 2)
36+
{
37+
if (bufferSize < minimumSize || bufferSize > MaxSectionBufferSize)
38+
{
39+
throw new ArgumentOutOfRangeException(
40+
nameof(bufferSize),
41+
bufferSize,
42+
$"Buffer size must be between {minimumSize} and {MaxSectionBufferSize} characters.");
43+
}
44+
}
45+
3346
/// <summary>
3447
/// Initializes a new instance of the <see cref="IniFile"/> class
3548
/// bound to the specified file path.
@@ -83,7 +96,7 @@ private static partial int NativeGetPrivateProfileString(
8396

8497
/// <summary>
8598
/// Retrieves an integer value from an INI file.
86-
/// If the key is not found or the value is not a valid integer, <paramref name="nDefault"/> is returned.
99+
/// If the key is not found, <paramref name="nDefault"/> is returned.
87100
/// </summary>
88101
[LibraryImport("kernel32", EntryPoint = "GetPrivateProfileIntW",
89102
StringMarshalling = StringMarshalling.Utf16)]
@@ -130,47 +143,67 @@ private static partial int NativeGetPrivateProfileSectionNames(
130143
/// <param name="key">The key name.</param>
131144
/// <param name="value">The string value to write.</param>
132145
/// <param name="section">
133-
/// The section name. Pass <c>null</c> to target the unnamed (global) area of the file.
146+
/// The section name. Pass <c>null</c> to target the empty section name, which Windows serializes as <c>[]</c>.
134147
/// </param>
135148
/// <returns><c>true</c> if the write succeeded; otherwise <c>false</c>.</returns>
149+
/// <exception cref="ArgumentException">
150+
/// Thrown when <paramref name="key"/> is <c>null</c>, empty, or whitespace.
151+
/// </exception>
136152
public bool Write(string key, string? value, string? section = null)
137-
=> NativeWritePrivateProfileString(section, key, value, _filePath);
153+
{
154+
ArgumentException.ThrowIfNullOrWhiteSpace(key);
155+
return NativeWritePrivateProfileString(NormalizeSection(section), key, value, _filePath);
156+
}
138157

139158
/// <summary>
140159
/// Reads a string value from the specified key in the given section of the INI file.
141160
/// </summary>
142161
/// <param name="key">
143162
/// The key name. If <c>null</c>, all key names in <paramref name="section"/> are returned
144-
/// as a single null-separated string.
163+
/// as a single null-separated string. If both <paramref name="key"/> and
164+
/// <paramref name="section"/> are <c>null</c>, all section names are returned.
145165
/// </param>
146166
/// <param name="section">
147-
/// The section name. If <c>null</c>, all section names in the file are returned.
167+
/// The section name. When <paramref name="key"/> is not <c>null</c>, pass <c>null</c>
168+
/// to target the empty section name, which Windows serializes as <c>[]</c>.
148169
/// </param>
149170
/// <param name="defaultValue">
150171
/// The value returned when the key is not found. Defaults to an empty string.
151172
/// </param>
152173
/// <param name="bufferSize">
153174
/// The initial size of the internal read buffer in characters. Defaults to <see cref="DefaultBufferSize"/>.
154175
/// The buffer doubles automatically if the value exceeds it, up to <see cref="MaxSectionBufferSize"/>.
176+
/// For native key/section enumeration, the buffer must be at least 3 characters.
155177
/// </param>
156178
/// <returns>
157179
/// The value associated with the key, or <paramref name="defaultValue"/> if the key was not found.
158180
/// </returns>
159-
public string ReadString(string key, string? section = null,
181+
public string ReadString(string? key, string? section = null,
160182
string defaultValue = "", int bufferSize = DefaultBufferSize)
161183
{
184+
ValidateBufferSize(bufferSize, key is null ? 3 : 2);
185+
186+
string? nativeSection = key is null ? section : NormalizeSection(section);
187+
162188
while (true)
163189
{
164190
char[] buffer = new char[bufferSize];
165-
int length = NativeGetPrivateProfileString(section, key, defaultValue, buffer, bufferSize, _filePath);
191+
int length = NativeGetPrivateProfileString(nativeSection, key, defaultValue, buffer, bufferSize, _filePath);
192+
int truncationLength = key is null ? bufferSize - 2 : bufferSize - 1;
166193

167-
// If length == bufferSize - 1, the buffer was too small and the value was truncated.
168-
if (length == bufferSize - 1 && bufferSize < MaxSectionBufferSize)
194+
// String reads signal truncation at nSize - 1; enumeration reads signal it at nSize - 2.
195+
if (length >= truncationLength && bufferSize < MaxSectionBufferSize)
169196
{
170197
bufferSize = Math.Min(bufferSize * 2, MaxSectionBufferSize);
171198
continue;
172199
}
173200

201+
if (key is null && length >= truncationLength)
202+
{
203+
throw new InvalidOperationException(
204+
$"INI data exceeds the maximum buffer size of {MaxSectionBufferSize} characters.");
205+
}
206+
174207
return new string(buffer, 0, length);
175208
}
176209
}
@@ -179,24 +212,29 @@ public string ReadString(string key, string? section = null,
179212
/// Reads an integer value from the specified key in the given section of the INI file.
180213
/// </summary>
181214
/// <param name="key">The key name.</param>
182-
/// <param name="section">The section name.</param>
215+
/// <param name="section">The section name, or <c>null</c> for the empty section name serialized as <c>[]</c>.</param>
183216
/// <param name="defaultValue">
184-
/// The value returned when the key is not found or its value is not a valid integer.
185-
/// Defaults to <c>-1</c>.
217+
/// The value returned when the key is not found. Defaults to <c>-1</c>.
186218
/// </param>
187219
/// <returns>
188-
/// The integer value of the key, or <paramref name="defaultValue"/> if the key was not found
189-
/// or could not be parsed. If the stored value starts with digits followed by non-digit
190-
/// characters (e.g. <c>"10apples"</c>), only the leading numeric portion is returned (<c>10</c>).
220+
/// The integer value of the key, or <paramref name="defaultValue"/> if the key was not found.
221+
/// Malformed numeric values follow the native <c>GetPrivateProfileInt</c> parsing behavior
222+
/// and may return <c>0</c> instead of <paramref name="defaultValue"/>.
191223
/// </returns>
224+
/// <exception cref="ArgumentException">
225+
/// Thrown when <paramref name="key"/> is <c>null</c>, empty, or whitespace.
226+
/// </exception>
192227
public int ReadInt(string key, string? section = null, int defaultValue = -1)
193-
=> NativeGetPrivateProfileInt(section, key, defaultValue, _filePath);
228+
{
229+
ArgumentException.ThrowIfNullOrWhiteSpace(key);
230+
return NativeGetPrivateProfileInt(NormalizeSection(section), key, defaultValue, _filePath);
231+
}
194232

195233
/// <summary>
196234
/// Reads a boolean value from the specified key in the given section of the INI file.
197235
/// </summary>
198236
/// <param name="key">The key name.</param>
199-
/// <param name="section">The section name.</param>
237+
/// <param name="section">The section name, or <c>null</c> for the empty section name serialized as <c>[]</c>.</param>
200238
/// <param name="defaultValue">
201239
/// The value returned when the key is not found or cannot be parsed. Defaults to <c>false</c>.
202240
/// </param>
@@ -207,6 +245,7 @@ public int ReadInt(string key, string? section = null, int defaultValue = -1)
207245
/// </returns>
208246
public bool ReadBool(string key, string? section = null, bool defaultValue = false)
209247
{
248+
ArgumentException.ThrowIfNullOrWhiteSpace(key);
210249
string value = ReadString(key, section);
211250

212251
if (string.IsNullOrWhiteSpace(value))
@@ -242,6 +281,7 @@ public bool ReadBool(string key, string? section = null, bool defaultValue = fal
242281
public string[] GetAllDataSection(string section, int bufferSize = MaxSectionBufferSize)
243282
{
244283
ArgumentException.ThrowIfNullOrWhiteSpace(section);
284+
ValidateBufferSize(bufferSize);
245285

246286
nint pMem = Marshal.AllocHGlobal(bufferSize * sizeof(char));
247287
try
@@ -283,6 +323,8 @@ public string[] GetAllDataSection(string section, int bufferSize = MaxSectionBuf
283323
/// </exception>
284324
public string[] GetAllSections(int bufferSize = MaxSectionBufferSize)
285325
{
326+
ValidateBufferSize(bufferSize);
327+
286328
nint pMem = Marshal.AllocHGlobal(bufferSize * sizeof(char));
287329
try
288330
{
@@ -313,38 +355,40 @@ public string[] GetAllSections(int bufferSize = MaxSectionBufferSize)
313355
/// Deletes the specified key (and its value) from the given section of the INI file.
314356
/// </summary>
315357
/// <param name="key">The key name to delete.</param>
316-
/// <param name="section">The section containing the key.</param>
358+
/// <param name="section">The section containing the key, or <c>null</c> for the empty section name serialized as <c>[]</c>.</param>
317359
/// <returns><c>true</c> if the operation succeeded; otherwise <c>false</c>.</returns>
318360
/// <exception cref="ArgumentException">
319361
/// Thrown when <paramref name="key"/> is <c>null</c>, empty, or whitespace.
320362
/// </exception>
321363
public bool DeleteKey(string key, string? section = null)
322364
{
323365
ArgumentException.ThrowIfNullOrWhiteSpace(key);
324-
return NativeWritePrivateProfileString(section, key, null, _filePath);
366+
return NativeWritePrivateProfileString(NormalizeSection(section), key, null, _filePath);
325367
}
326368

327369
/// <summary>
328370
/// Deletes the specified section and all of its keys from the INI file.
329371
/// </summary>
330-
/// <param name="section">The section name to delete.</param>
372+
/// <param name="section">The section name to delete, or <c>null</c> for the empty section name serialized as <c>[]</c>.</param>
331373
/// <returns><c>true</c> if the operation succeeded; otherwise <c>false</c>.</returns>
332374
public bool DeleteSection(string? section = null)
333-
=> NativeWritePrivateProfileString(section, null, null, _filePath);
375+
=> NativeWritePrivateProfileString(NormalizeSection(section), null, null, _filePath);
334376

335377
/// <summary>
336378
/// Checks whether the specified key exists in the given section of the INI file.
337379
/// </summary>
338380
/// <param name="key">The key name to check.</param>
339-
/// <param name="section">The section containing the key.</param>
381+
/// <param name="section">The section containing the key, or <c>null</c> for the empty section name serialized as <c>[]</c>.</param>
340382
/// <returns>
341383
/// <c>true</c> if the key exists (even if its value is empty); otherwise <c>false</c>.
342384
/// </returns>
343385
public bool KeyExists(string key, string? section = null)
344386
{
387+
ArgumentException.ThrowIfNullOrWhiteSpace(key);
388+
345389
char[] buffer = new char[MaxSectionBufferSize];
346390
// Passing null for lpKeyName returns all key names in the section separated by \0.
347-
int length = NativeGetPrivateProfileString(section, null, null, buffer, MaxSectionBufferSize, _filePath);
391+
int length = NativeGetPrivateProfileString(NormalizeSection(section), null, null, buffer, MaxSectionBufferSize, _filePath);
348392
if (length <= 0) return false;
349393

350394
string raw = new string(buffer, 0, length);

0 commit comments

Comments
 (0)