/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Amazon.Util;
using Amazon.Util.Internal;
namespace Amazon.Runtime.Internal.Util
{
///
/// A NestedProperty is a value in a configuration file that has a parent key
/// and one or more key value pairs associate with it.
/// For example
/// s3 =
/// max_retries = 10
/// s3 is the parent key, SubpropertyKeys contains ["max_retries"] and SubpropertyValues
/// contains ["10"]
///
internal class NestedProperty
{
public string ParentKey { get; set; }
public List SubpropertyKeys { get; set; } = new List ();
public List SubpropertyValues { get; set; } = new List ();
}
///
/// Provides read/write access to a file in the INI format.
///
/// This class is not threadsafe.
///
public class IniFile
{
#if BCL35
private class Tuple
{
internal T1 Item1 { get; private set; }
internal T2 Item2 { get; private set; }
internal Tuple(T1 item1, T2 item2)
{
Item1 = item1;
Item2 = item2;
}
}
#endif
private const string sectionNamePrefix = "[";
private const string sectionNameSuffix = "]";
private const string keyValueSeparator = "=";
private const string semiColonComment = ";";
private const string hashComment = "#";
private OptimisticLockedTextFile textFile;
private Logger logger;
///
/// Construct a new IniFile.
///
/// path of the IniFile
public IniFile(string filePath)
{
logger = Logger.GetLogger(GetType());
textFile = new OptimisticLockedTextFile(filePath);
Validate();
}
///
/// the path of the file
///
public String FilePath
{
get
{
return textFile.FilePath;
}
}
///
/// helper to access the lines of the file
///
private List Lines
{
get
{
return textFile.Lines;
}
}
///
/// Persist the changes to this INI file to disk.
///
public void Persist()
{
Validate();
textFile.Persist();
}
///
/// Rename the section fromSectionName to toSectionName
///
///
///
public void RenameSection(string oldSectionName, string newSectionName)
{
RenameSection(oldSectionName, newSectionName, false);
}
///
/// Rename the section fromSectionName to toSectionName
///
///
///
/// if true and destination section already exists overwrite it
public void RenameSection(string oldSectionName, string newSectionName, bool force)
{
int sectionLineNumber = 0;
if (TrySeekSection(oldSectionName, ref sectionLineNumber))
{
int lineNumber = 0;
if (TrySeekSection(newSectionName, ref lineNumber))
{
// if oldSectionName == newSectionName it's a no op
if (!string.Equals(oldSectionName, newSectionName, StringComparison.Ordinal))
{
if (force)
{
DeleteSection(newSectionName);
// recursive call with force == false now that the destination section is gone
RenameSection(oldSectionName, newSectionName, false);
}
else
throw new ArgumentException("Cannot rename section. The destination section " + newSectionName +
" already exists." + GetLineMessage(lineNumber));
}
}
else
Lines[sectionLineNumber] = sectionNamePrefix + newSectionName + sectionNameSuffix;
}
else
throw new ArgumentException("Cannot rename section. The source section " + oldSectionName + " does not exist.");
}
///
/// Copy the section fromSectionName to toSectionName
///
///
///
/// Any properties in the original section that are also in this dictionary will
/// be replaced by the value from this dictionary.
public void CopySection(string fromSectionName, string toSectionName, Dictionary replaceProperties)
{
CopySection(fromSectionName, toSectionName, replaceProperties, false);
}
///
/// Copy the section fromSectionName to toSectionName
///
///
///
/// Any properties in the original section that are also in this dictionary will
/// be replaced by the value from this dictionary.
/// if true and destination section already exists overwrite it
public void CopySection(string fromSectionName, string toSectionName, Dictionary replaceProperties, bool force)
{
int currentLineNumber = 0;
if (TrySeekSection(fromSectionName, ref currentLineNumber))
{
int lineNumber = 0;
if (TrySeekSection(toSectionName, ref lineNumber))
{
// if fromSectionName == toSectionName it's a no op
if (!string.Equals(fromSectionName, toSectionName, StringComparison.Ordinal))
{
if (force)
{
DeleteSection(toSectionName);
// recursive call with force == false now that the destination section is gone
CopySection(fromSectionName, toSectionName, replaceProperties, false);
}
else
throw new ArgumentException("Cannot copy section. The destination section " + toSectionName +
" already exists." + GetLineMessage(lineNumber));
}
}
else
{
// keep the first line number
var firstLineNumber = currentLineNumber;
// find the last line number (exclusive)
// could be end of file or beginning of next section
string dummySectionName;
currentLineNumber++;
while (currentLineNumber < Lines.Count
&& !TryParseSection(Lines[currentLineNumber], out dummySectionName))
{
currentLineNumber++;
}
// add the new section header to the end of the file
Lines.Add(sectionNamePrefix + toSectionName + sectionNameSuffix);
// copy the contents of the section to the end of the file
for (int line = firstLineNumber + 1; line < currentLineNumber; line++)
{
// If the key is in replaceProperties use the value from there
// otherwise just copy the line.
string propertyName;
string unused;
if (TryParseProperty(Lines[line], out propertyName, out unused) &&
replaceProperties.ContainsKey(propertyName))
Lines.Add(GetPropertyLine(propertyName, replaceProperties[propertyName]));
else
Lines.Add(Lines[line]);
}
}
}
else
throw new ArgumentException("Cannot copy section. The source section " + fromSectionName + " does not exist.");
}
///
/// Update the section with the properties given.
/// If the section doesn't exist, it will be appended to the file.
///
/// Notes:
/// 1. Any properties that do exist in the section will be updated.
/// 2. A null value for a property denotes that it should be deleted from the section
/// 3. If any properties don't exist they will be appended to the end of the section
/// in the same order they appear in the SortedDictionary.
///
/// name of the section to operate on
/// properties to add/update/delete
public virtual void EditSection(string sectionName, SortedDictionary properties)
{
EnsureSectionExists(sectionName);
// build dictionary from list
// ensure the keys will be looked up case-insensitive
var propertiesLookup = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var pair in properties)
{
propertiesLookup.Add(pair.Key, pair.Value);
}
var lineNumber = 0;
if (TrySeekSection(sectionName, ref lineNumber))
{
lineNumber++;
string propertyName;
string propertyValue;
NestedProperty nestedProperty;
while (SeekProperty(ref lineNumber, out propertyName, out propertyValue, out nestedProperty))
{
var propertyDeleted = false;
if (propertiesLookup.ContainsKey(propertyName))
{
if (!string.Equals(propertiesLookup[propertyName], propertyValue))
{
if (propertiesLookup[propertyName] == null)
{
// delete the line
Lines.RemoveAt(lineNumber);
propertyDeleted = true;
}
else
{
// update the line
Lines[lineNumber] = GetPropertyLine(propertyName, propertiesLookup[propertyName]);
}
}
propertiesLookup.Remove(propertyName);
}
if (!propertyDeleted)
{
lineNumber++;
}
}
foreach (var pair in properties)
{
if (propertiesLookup.ContainsKey(pair.Key) && propertiesLookup[pair.Key] != null)
{
Lines.Insert(lineNumber++, pair.Key + keyValueSeparator + pair.Value);
}
}
}
}
///
/// Check if the section exists. If not, append it to the end of the file.
///
/// section to ensure exists
public void EnsureSectionExists(string sectionName)
{
var lineNumber = 0;
if (!TrySeekSection(sectionName, ref lineNumber))
{
Lines.Add(sectionNamePrefix + sectionName + sectionNameSuffix);
}
}
///
/// If the section exists, delete it from the INI file.
///
/// section to delete
public void DeleteSection(string sectionName)
{
var lineNumber = 0;
if (TrySeekSection(sectionName, ref lineNumber))
{
Lines.RemoveAt(lineNumber);
while (lineNumber < Lines.Count &&
!IsSection(Lines[lineNumber]))
{
Lines.RemoveAt(lineNumber);
}
}
}
public virtual HashSet ListSectionNames()
{
var sectionNames = new HashSet();
int lineNumber = 0;
string sectionName = null;
while (SeekSection(ref lineNumber, out sectionName))
{
sectionNames.Add(sectionName);
lineNumber++;
}
return sectionNames;
}
///
/// Determine if a section exists in the INI file.
///
/// name of section to look for
/// true if the section exists, false otherwise
public bool SectionExists(string sectionName)
{
var lineNumber = 0;
return TrySeekSection(sectionName, ref lineNumber);
}
///
/// Determine if a section exists in the INI file.
///
/// Regex to match name of section to look for
/// name of section if regex matches
/// true if the section exists, false otherwise
public bool SectionExists(Regex sectionNameRegex, out string sectionName)
{
var lineNumber = 0;
return TrySeekSection(sectionNameRegex, ref lineNumber, out sectionName);
}
///
/// Return the properties for the section if it exists.
///
/// name of section to get
/// properties contained in the section
/// True if the section was found, false otherwise
public virtual bool TryGetSection(string sectionName, out Dictionary properties)
{
var lineNumber = 0;
properties = new Dictionary(StringComparer.OrdinalIgnoreCase);
if (TrySeekSection(sectionName, ref lineNumber))
{
lineNumber++;
string propertyName;
string propertyValue;
NestedProperty nestedProperty;
while (SeekProperty(ref lineNumber, out propertyName, out propertyValue, out nestedProperty))
{
if (IsDuplicateProperty(properties, propertyName, sectionName, lineNumber))
{
properties.Clear();
return false;
}
properties.Add(propertyName, propertyValue);
lineNumber++;
}
return true;
}
return false;
}
///
/// Return the properties for the section if it exists.
///
/// Regex to match name of section to get
/// properties contained in the section
/// True if the section was found, false otherwise
public bool TryGetSection(Regex sectionNameRegex, out Dictionary properties)
{
string dummy = null;
return TryGetSection(sectionNameRegex, out _, out properties);
}
///
/// Returns the properties and subproperties for a section if it exists
///
///
///
///
///
public bool TryGetSection(Regex sectionNameRegex, out Dictionary properties, out Dictionary> nestedProperties)
{
string dummy = null;
return TryGetSection(sectionNameRegex, out _, out properties, out nestedProperties);
}
///
/// Return the properties for the section if it exists.
///
/// Regex to match name of section to get
/// name of section if regex matches
/// properties contained in the section
/// True if the section was found, false otherwise
public bool TryGetSection(Regex sectionNameRegex, out string sectionName,
out Dictionary properties)
{
var lineNumber = 0;
properties = new Dictionary(StringComparer.OrdinalIgnoreCase);
if (TrySeekSection(sectionNameRegex, ref lineNumber, out sectionName))
{
lineNumber++;
string propertyName;
string propertyValue;
NestedProperty nestedProperty;
while (SeekProperty(ref lineNumber, out propertyName, out propertyValue, out nestedProperty))
{
if (IsDuplicateProperty(properties, propertyName, sectionName, lineNumber))
{
sectionName = null;
properties.Clear();
return false;
}
properties.Add(propertyName, propertyValue);
lineNumber++;
}
return true;
}
return false;
}
///
/// Return the properties for the section if it exists.
///
/// Regex to match name of section to get
/// name of section if regex matches
/// properties contained in the section
/// properties with nested attributes in the section
/// For example, for a profile with the following values:
/// [profile foo]
/// s3 =
/// max_retries = 10
/// max_concurrent_requests_ = 50
/// nestedProperties will contain:{s3: {max_retries: 10}, {max_concurrent_requests:50}}
/// True if the section was found, false otherwise
public bool TryGetSection(Regex sectionNameRegex, out string sectionName,
out Dictionary properties, out Dictionary> nestedProperties)
{
var lineNumber = 0;
properties = new Dictionary(StringComparer.OrdinalIgnoreCase);
nestedProperties = new Dictionary>(StringComparer.OrdinalIgnoreCase);
if (TrySeekSection(sectionNameRegex, ref lineNumber, out sectionName))
{
lineNumber++;
string propertyName;
string propertyValue;
NestedProperty nestedProperty;
while (SeekProperty(ref lineNumber, out propertyName, out propertyValue, out nestedProperty))
{
if (IsDuplicateProperty(properties, propertyName, sectionName, lineNumber))
{
sectionName = null;
properties.Clear();
return false;
}
if(nestedProperty?.ParentKey != null)
{
// Following the example in the summary section, nestedProperty would look like
// nestedProperty.ParentKey = "s3"
// nestedProperty.SubpropertyKeys = ["max_retries","max_concurrent_requests"]
// nestedProperty.SubpropertyValues = ["10","50"]
#if BCL35
List> keyValuePairs = InternalSDKUtils.Zip(nestedProperty.SubpropertyKeys,nestedProperty.SubpropertyValues, (k, v) => new Tuple(k, v)).ToList();
#else
List> keyValuePairs = nestedProperty.SubpropertyKeys.Zip(nestedProperty.SubpropertyValues, (k, v) => new Tuple(k, v)).ToList();
#endif
foreach (var keyValuePair in keyValuePairs)
{
if (nestedProperties.ContainsKey(nestedProperty.ParentKey))
{
nestedProperties[nestedProperty.ParentKey][keyValuePair.Item1] = keyValuePair.Item2;
}
else
{
nestedProperties.Add(nestedProperty.ParentKey, new Dictionary() {{ keyValuePair.Item1, keyValuePair.Item2 }});
}
}
}
else
{
properties.Add(propertyName, propertyValue);
}
lineNumber++;
}
return true;
}
return false;
}
override public string ToString()
{
return textFile.ToString();
}
private bool IsDuplicateProperty(Dictionary properties, string propertyName, string sectionName, int lineNumber)
{
var result = properties.ContainsKey(propertyName);
if (result)
logger.InfoFormat("Skipping section {0} because of duplicate property {1}. {2}", sectionName, propertyName, GetLineMessage(lineNumber));
return result;
}
private void Validate()
{
for (int i = 0; i < Lines.Count; i++)
{
var line = Lines[i];
if (!IsProperty(line) && !IsSection(line) && !IsCommentOrBlank(line))
{
throw new InvalidDataException(GetErrorMessage(i));
}
}
}
private bool TrySeekSection(Regex sectionNameRegex, ref int lineNumber, out string sectionName)
{
string currentSectionName = null;
while (SeekSection(ref lineNumber, out currentSectionName) &&
!sectionNameRegex.IsMatch(currentSectionName))
{
lineNumber++;
}
sectionName = currentSectionName;
return currentSectionName != null && sectionNameRegex.IsMatch(currentSectionName);
}
private bool TrySeekSection(String sectionName, ref int lineNumber)
{
string currentSectionName = null;
while (SeekSection(ref lineNumber, out currentSectionName) &&
!string.Equals(sectionName, currentSectionName, StringComparison.Ordinal))
{
lineNumber++;
}
return string.Equals(sectionName, currentSectionName, StringComparison.Ordinal);
}
private bool SeekSection(ref int lineNumber, out string sectionName)
{
while (lineNumber < Lines.Count)
{
if (TryParseSection(Lines[lineNumber], out sectionName))
{
return true;
}
lineNumber++;
}
sectionName = null;
return false;
}
private bool SeekProperty(ref int lineNumber, out string propertyName, out string propertyValue, out NestedProperty nestedProperty)
{
while (lineNumber < Lines.Count)
{
nestedProperty = null;
if (TryParseProperty(Lines[lineNumber], out propertyName, out propertyValue))
{
//if propertyValue is empty, that means that it is a continuation property
if (String.IsNullOrEmpty(propertyValue))
{
lineNumber++;
TryParseSubproperties(ref lineNumber, propertyName, out nestedProperty);
}
return true;
}
else if (IsSection(Lines[lineNumber]))
{
return false;
}
else if (IsCommentOrBlank(Lines[lineNumber]))
{
lineNumber++;
}
else
{
throw new InvalidDataException(GetErrorMessage(lineNumber));
}
}
nestedProperty = null;
propertyName = null;
propertyValue = null;
return false;
}
private bool TryParseSubproperties(ref int lineNumber, string propertyName, out NestedProperty nestedProperty)
{
nestedProperty = new NestedProperty();
while (lineNumber < Lines.Count)
{
string currentLine = Lines[lineNumber];
string trimmedLine = currentLine.Trim();
string subpropertyName;
string subpropertyValue;
if (!StartsWithWhitespace(currentLine) || IsSection(currentLine))
{
lineNumber--;
return false;
}
else if (IsCommentOrBlank(currentLine))
{
}
else if (StartsWithWhitespace(currentLine))
{
var separatorIndex = trimmedLine.IndexOf(keyValueSeparator, StringComparison.Ordinal);
subpropertyName = trimmedLine.Substring(0, separatorIndex).Trim();
subpropertyValue = trimmedLine.Substring(separatorIndex + 1).Trim();
nestedProperty.SubpropertyKeys.Add(subpropertyName);
nestedProperty.SubpropertyValues.Add(subpropertyValue);
}
else
{
throw new InvalidDataException(GetErrorMessage(lineNumber));
}
lineNumber++;
nestedProperty.ParentKey = propertyName;
}
return true;
}
private string GetErrorMessage(int lineNumber)
{
return string.Format(CultureInfo.InvariantCulture,
"Line {0}:<{1}> in file {2} does not contain a section, property or comment.",
lineNumber + 1,
Lines[lineNumber],
FilePath);
}
private static bool IsCommentOrBlank(string line)
{
if (line == null)
{
return true;
}
else
{
line = line.Trim();
return string.IsNullOrEmpty(line)
|| line.StartsWith(semiColonComment, StringComparison.Ordinal)
|| line.StartsWith(hashComment, StringComparison.Ordinal);
}
}
private static bool IsSection(string line)
{
string dummy;
return TryParseSection(line, out dummy);
}
private static bool TryParseSection(string line, out string sectionName)
{
if (line != null)
{
line = line.Trim();
if (line.StartsWith(sectionNamePrefix, StringComparison.Ordinal)
&& line.EndsWith(sectionNameSuffix, StringComparison.Ordinal))
{
sectionName = line.Substring(1, line.Length - 2).Trim();
return true;
}
}
sectionName = null;
return false;
}
private static bool IsProperty(string line)
{
string dummyName;
string dummyValue;
return TryParseProperty(line, out dummyName, out dummyValue);
}
private static bool TryParseProperty(string line, out string propertyName, out string propertyValue)
{
if (line != null && !IsCommentOrBlank(line))
{
line = line.Trim();
var separatorIndex = line.IndexOf(keyValueSeparator, StringComparison.Ordinal);
if (separatorIndex >= 0)
{
propertyName = line.Substring(0, separatorIndex).Trim();
var valueStartIndex = separatorIndex + keyValueSeparator.Length;
propertyValue = line.Substring(valueStartIndex, line.Length - valueStartIndex).Trim();
return true;
}
}
propertyName = null;
propertyValue = null;
return false;
}
private static string GetPropertyLine(string propertyName, string propertyValue)
{
return string.Concat(propertyName, keyValueSeparator, propertyValue);
}
private string GetLineMessage(int lineNumber)
{
return string.Concat("(", this.FilePath, ":line ", lineNumber + 1, ")");
}
private static bool StartsWithWhitespace(string line)
{
return line.Length > 0 && char.IsWhiteSpace(line[0]);
}
}
}