Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,25 @@
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddIniFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange) { throw null; }
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddIniStream(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.IO.Stream stream) { throw null; }
}
public static partial class WritableIniConfigurationExtensions
{
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddWritableIniFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string path, bool optional = true) { throw null; }
}
}
namespace Microsoft.Extensions.Configuration.Ini
{
public sealed partial class IniDocument
{
public IniDocument() { }
public System.Collections.Generic.IReadOnlyList<string> Sections { get { throw null; } }
public System.Collections.Generic.IReadOnlyList<(string Key, string Value)> Entries(string section) { throw null; }

Check failure on line 29 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop (Build linux-x64 debug Libraries_WithPackages)

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L29

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(29,57): error CS8137: (NETCORE_ENGINEERING_TELEMETRY=Build) Cannot define a class or member that utilizes tuples because the compiler required type 'System.Runtime.CompilerServices.TupleElementNamesAttribute' cannot be found. Are you missing a reference?

Check failure on line 29 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop (Build linux-x64 debug Libraries_WithPackages)

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L29

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(29,57): error CS8179: (NETCORE_ENGINEERING_TELEMETRY=Build) Predefined type 'System.ValueTuple`2' is not defined or imported
public string? Get(string section, string key) { throw null; }
public static Microsoft.Extensions.Configuration.Ini.IniDocument Parse(string text) { throw null; }
public bool Remove(string section, string key) { throw null; }
public void Set(string section, string key, string value, bool? quote = default(bool?)) { }
public override string ToString() { throw null; }
public string ToIniString() { throw null; }
}
public partial class IniConfigurationProvider : Microsoft.Extensions.Configuration.FileConfigurationProvider
{
public IniConfigurationProvider(Microsoft.Extensions.Configuration.Ini.IniConfigurationSource source) : base (default(Microsoft.Extensions.Configuration.FileConfigurationSource)) { }
Expand All @@ -39,4 +55,19 @@
public IniStreamConfigurationSource() { }
public override Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
}
public sealed partial class WritableIniConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider
{
public WritableIniConfigurationProvider(Microsoft.Extensions.Configuration.Ini.WritableIniConfigurationSource source) { }
public string Path { get { throw null; } }
public override void Load() { }
public void Save() { }
public void SetValue(string section, string key, string value, bool? quote = default(bool?)) { }
}
public sealed partial class WritableIniConfigurationSource : Microsoft.Extensions.Configuration.IConfigurationSource

Check failure on line 66 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop (Build linux-x64 debug Libraries_WithPackages)

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L66

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(66,33): error CS0656: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing compiler required member 'System.Runtime.CompilerServices.RequiredMemberAttribute..ctor'

Check failure on line 66 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop (Build linux-x64 debug Libraries_WithPackages)

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L66

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(66,33): error CS0656: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing compiler required member 'System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute..ctor'

Check failure on line 66 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop (Build linux-x64 debug Libraries_WithPackages)

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L66

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(66,33): error CS0656: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing compiler required member 'System.Runtime.CompilerServices.RequiredMemberAttribute..ctor'

Check failure on line 66 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop (Build Source-Build (Linux_x64))

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L66

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(66,33): error CS0656: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing compiler required member 'System.Runtime.CompilerServices.RequiredMemberAttribute..ctor'

Check failure on line 66 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop (Build Source-Build (Linux_x64))

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L66

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(66,33): error CS0656: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing compiler required member 'System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute..ctor'

Check failure on line 66 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L66

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(66,33): error CS0656: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing compiler required member 'System.Runtime.CompilerServices.RequiredMemberAttribute..ctor'

Check failure on line 66 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L66

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(66,33): error CS0656: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing compiler required member 'System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute..ctor'

Check failure on line 66 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L66

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(66,33): error CS0656: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing compiler required member 'System.Runtime.CompilerServices.RequiredMemberAttribute..ctor'

Check failure on line 66 in src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs

View check run for this annotation

Azure Pipelines / runtime-dev-innerloop

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs#L66

src/libraries/Microsoft.Extensions.Configuration.Ini/ref/Microsoft.Extensions.Configuration.Ini.cs(66,33): error CS0656: (NETCORE_ENGINEERING_TELEMETRY=Build) Missing compiler required member 'System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute..ctor'
{
public WritableIniConfigurationSource() { }
public bool Optional { get { throw null; } set { } }
public required string Path { get { throw null; } set { } }
public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Microsoft.Extensions.Configuration.Ini
{
/// <summary>
/// A lossless, editable model of an INI file: a sequence of <c>[Section]</c>
/// blocks whose entries are <c>Key=value</c> lines, where integer-style values
/// are written bare (<c>VICIIModel=3</c>) and string values may be
/// double-quoted (<c>WIC64MACAddress="08:d1:f9:0a:0c:0e"</c>).
///
/// This is the write engine the built-in <see cref="IniStreamConfigurationProvider"/>
/// lacks. Because callers frequently edit a config file they <em>share</em>
/// with another application, the document preserves every section, key, order,
/// and quoting it parsed: a read-modify-write round-trips losslessly and never
/// drops resources the caller does not itself manage.
///
/// (Comment lines and blank-line layout are not preserved on write; the read
/// path ignores them, matching the built-in provider.)
/// </summary>
public sealed class IniDocument
{
private sealed class IniEntry
{
public required string Key { get; init; }
public string Value { get; set; } = string.Empty;
public bool Quoted { get; set; }
}

private sealed class IniSection
{
public required string Name { get; init; }
public List<IniEntry> Entries { get; } = new();
}

private readonly List<IniSection> _sections = new();

/// <summary>Section names in document order.</summary>
public IReadOnlyList<string> Sections => _sections.ConvertAll(s => s.Name);

/// <summary>Parse INI text into an editable, round-trippable document.</summary>
public static IniDocument Parse(string text)
{
ArgumentNullException.ThrowIfNull(text);

var document = new IniDocument();
IniSection? current = null;

foreach (var rawLine in text.Split('\n'))
{
var line = rawLine.TrimEnd('\r').Trim();
if (line.Length == 0)
continue;

// Skip comment lines (parity with the built-in INI read path).
if (line[0] is ';' or '#' or '/')
continue;

if (line[0] == '[' && line[^1] == ']')
{
var name = line[1..^1].Trim();
current = document.GetOrAddSection(name);
continue;
}

var eq = line.IndexOf('=');
if (eq <= 0 || current is null)
continue;

var key = line[..eq].Trim();
var rawValue = line[(eq + 1)..].Trim();
var quoted = rawValue.Length >= 2 && rawValue[0] == '"' && rawValue[^1] == '"';
var value = quoted ? rawValue[1..^1] : rawValue;

current.Entries.Add(new IniEntry { Key = key, Value = value, Quoted = quoted });
}

return document;
}

/// <summary>Get a value (quotes stripped), or null if the section/key is absent.</summary>
public string? Get(string section, string key)
{
var entry = FindSection(section)?.Entries
.FirstOrDefault(e => string.Equals(e.Key, key, StringComparison.Ordinal));
return entry?.Value;
}

/// <summary>
/// Set a value, updating an existing entry in place or appending a new one
/// (creating the section if needed). <paramref name="quote"/>: true forces
/// double-quoting, false forces bare; null (the default) preserves the
/// existing entry's quoting on update and writes bare for a new entry.
/// Preserving on update is what lets a read-modify-write round-trip a shared
/// file without unquoting its string values.
/// </summary>
public void Set(string section, string key, string value, bool? quote = null)
{
ArgumentNullException.ThrowIfNull(value);

var target = GetOrAddSection(section);
var entry = target.Entries
.FirstOrDefault(e => string.Equals(e.Key, key, StringComparison.Ordinal));
if (entry is null)
{
target.Entries.Add(new IniEntry { Key = key, Value = value, Quoted = quote ?? false });
return;
}

entry.Value = value;
if (quote.HasValue)
entry.Quoted = quote.Value;
}

/// <summary>Remove a value. Returns true if it existed.</summary>
public bool Remove(string section, string key)
{
var target = FindSection(section);
return target is not null
&& target.Entries.RemoveAll(e => string.Equals(e.Key, key, StringComparison.Ordinal)) > 0;
}

/// <summary>Entries of a section in order, with quotes stripped.</summary>
public IReadOnlyList<(string Key, string Value)> Entries(string section)
{
var target = FindSection(section);
return target is null ? Array.Empty<(string, string)>() : target.Entries.ConvertAll(e => (e.Key, e.Value));
}

/// <summary>Serialize back to INI text (sections in order, blank line between each).</summary>
public string ToIniString()
{
var builder = new StringBuilder();
foreach (var section in _sections)
{
builder.Append('[').Append(section.Name).Append(']').Append('\n');
foreach (var entry in section.Entries)
{
builder.Append(entry.Key).Append('=');
if (entry.Quoted)
builder.Append('"').Append(entry.Value).Append('"');
else
builder.Append(entry.Value);
builder.Append('\n');
}

builder.Append('\n');
}

return builder.ToString();
}

/// <summary>Returns this document serialized as INI text.</summary>
public override string ToString() => ToIniString();

private IniSection? FindSection(string section) =>
_sections.FirstOrDefault(s => string.Equals(s.Name, section, StringComparison.Ordinal));

private IniSection GetOrAddSection(string section)
{
var existing = FindSection(section);
if (existing is not null)
return existing;

var created = new IniSection { Name = section };
_sections.Add(created);
return created;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.Configuration.Ini;

namespace Microsoft.Extensions.Configuration
{
/// <summary>
/// Extension methods for adding a writable INI configuration source.
/// </summary>
public static class WritableIniConfigurationExtensions
{
/// <summary>
/// Adds a writable INI file at <paramref name="path"/> to
/// <paramref name="builder"/>. The source resolves to a
/// <see cref="Ini.WritableIniConfigurationProvider"/> whose
/// <see cref="Ini.WritableIniConfigurationProvider.Save"/> writes changes
/// back losslessly.
/// </summary>
/// <param name="builder">The builder to add to.</param>
/// <param name="path">The physical path of the INI file.</param>
/// <param name="optional">
/// When true (default) a missing file yields empty configuration; when false
/// loading a missing file throws.
/// </param>
/// <returns>The <paramref name="builder"/>.</returns>
public static IConfigurationBuilder AddWritableIniFile(
this IConfigurationBuilder builder, string path, bool optional = true)
{
ArgumentNullException.ThrowIfNull(builder);
if (string.IsNullOrEmpty(path))
throw new ArgumentException(SR.Error_InvalidFilePath, nameof(path));

return builder.Add(new Ini.WritableIniConfigurationSource { Path = path, Optional = optional });
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;

namespace Microsoft.Extensions.Configuration.Ini
{
/// <summary>
/// The writable provider behind <see cref="WritableIniConfigurationSource"/>.
/// <see cref="Load"/> parses the INI into configuration data (identical keys to
/// the built-in provider); <see cref="Save"/> writes the current data back via
/// <see cref="IniDocument"/>, so unmanaged sections/keys and value quoting are
/// preserved across a read-modify-write.
/// </summary>
public sealed class WritableIniConfigurationProvider : ConfigurationProvider
{
private readonly WritableIniConfigurationSource _source;
private readonly Dictionary<string, bool> _quoting = new(StringComparer.OrdinalIgnoreCase);

/// <summary>Initializes a new instance for the given source.</summary>
public WritableIniConfigurationProvider(WritableIniConfigurationSource source)
{
ArgumentNullException.ThrowIfNull(source);
_source = source;
}

/// <summary>The INI file this provider reads from and writes to.</summary>
public string Path => _source.Path;

/// <summary>
/// Loads the INI file into configuration data. A missing file yields empty
/// data when the source is optional, otherwise throws.
/// </summary>
public override void Load()
{
var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);

if (File.Exists(_source.Path))
{
var document = IniDocument.Parse(File.ReadAllText(_source.Path));
foreach (var section in document.Sections)
{
foreach (var (key, value) in document.Entries(section))
data[ToConfigKey(section, key)] = value;
}
}
else if (!_source.Optional)
{
throw new FileNotFoundException($"Required INI file not found: {_source.Path}", _source.Path);
}

Data = data;
}

/// <summary>
/// Sets a resource. <paramref name="quote"/> controls how <see cref="Save"/>
/// writes it: true = double-quoted, false = bare, null = preserve the
/// existing entry's quoting (or bare if new).
/// </summary>
public void SetValue(string section, string key, string value, bool? quote = null)
{
var configKey = ToConfigKey(section, key);
Set(configKey, value);
if (quote.HasValue)
_quoting[configKey] = quote.Value;
}

/// <summary>
/// Writes the current configuration back to the INI file via a
/// read-modify-write: existing resources keep their quoting and any
/// resources not present in the data are preserved verbatim.
/// </summary>
public void Save()
{
var document = File.Exists(_source.Path)
? IniDocument.Parse(File.ReadAllText(_source.Path))
: new IniDocument();

foreach (var (configKey, value) in Data)
{
if (value is null || !TrySplitConfigKey(configKey, out var section, out var resource))
continue;

var quote = _quoting.TryGetValue(configKey, out var q) ? q : (bool?)null;
document.Set(section, resource, value, quote);
}

var directory = System.IO.Path.GetDirectoryName(_source.Path);
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);

File.WriteAllText(_source.Path, document.ToIniString());
}

private static string ToConfigKey(string section, string key) =>
$"{section}{ConfigurationPath.KeyDelimiter}{key}";

private static bool TrySplitConfigKey(string configKey, out string section, out string resource)
{
var index = configKey.IndexOf(ConfigurationPath.KeyDelimiter, StringComparison.Ordinal);
if (index <= 0 || index >= configKey.Length - 1)
{
section = string.Empty;
resource = string.Empty;
return false;
}

section = configKey[..index];
resource = configKey[(index + 1)..];
return true;
}
}
}
Loading
Loading