Files
libremetaverse/ExtensionLoader/Ini/IniReader.cs
2008-10-29 20:38:51 +00:00

652 lines
15 KiB
C#

#region Copyright
//
// Nini Configuration Project.
// Copyright (C) 2006 Brent R. Matzelle. All rights reserved.
//
// This software is published under the terms of the MIT X11 license, a copy of
// which has been included with this distribution in the LICENSE.txt file.
//
#endregion
using System;
using System.IO;
using System.Text;
using System.Collections;
namespace ExtensionLoader.Ini
{
#region IniReadState enumeration
/// <include file='IniReader.xml' path='//Enum[@name="IniReadState"]/docs/*' />
public enum IniReadState : int
{
/// <include file='IniReader.xml' path='//Enum[@name="IniReadState"]/Value[@name="Closed"]/docs/*' />
Closed,
/// <include file='IniReader.xml' path='//Enum[@name="IniReadState"]/Value[@name="EndOfFile"]/docs/*' />
EndOfFile,
/// <include file='IniReader.xml' path='//Enum[@name="IniReadState"]/Value[@name="Error"]/docs/*' />
Error,
/// <include file='IniReader.xml' path='//Enum[@name="IniReadState"]/Value[@name="Initial"]/docs/*' />
Initial,
/// <include file='IniReader.xml' path='//Enum[@name="IniReadState"]/Value[@name="Interactive"]/docs/*' />
Interactive
};
#endregion
#region IniType enumeration
/// <include file='IniReader.xml' path='//Enum[@name="IniType"]/docs/*' />
public enum IniType : int
{
/// <include file='IniReader.xml' path='//Enum[@name="IniType"]/Value[@name="Section"]/docs/*' />
Section,
/// <include file='IniReader.xml' path='//Enum[@name="IniType"]/Value[@name="Key"]/docs/*' />
Key,
/// <include file='IniReader.xml' path='//Enum[@name="IniType"]/Value[@name="Empty"]/docs/*' />
Empty
}
#endregion
/// <include file='IniReader.xml' path='//Class[@name="IniReader"]/docs/*' />
public class IniReader : IDisposable
{
#region Private variables
int lineNumber = 1;
int column = 1;
IniType iniType = IniType.Empty;
TextReader textReader = null;
bool ignoreComments = false;
StringBuilder name = new StringBuilder ();
StringBuilder value = new StringBuilder ();
StringBuilder comment = new StringBuilder ();
IniReadState readState = IniReadState.Initial;
bool hasComment = false;
bool disposed = false;
bool lineContinuation = false;
bool acceptCommentAfterKey = true;
bool acceptNoAssignmentOperator = false;
bool consumeAllKeyText = false;
char[] commentDelimiters = new char[] { ';' };
char[] assignDelimiters = new char[] { '=' };
#endregion
#region Public properties
/// <include file='IniReader.xml' path='//Property[@name="Name"]/docs/*' />
public string Name
{
get { return this.name.ToString (); }
}
/// <include file='IniReader.xml' path='//Property[@name="Value"]/docs/*' />
public string Value
{
get { return this.value.ToString (); }
}
/// <include file='IniReader.xml' path='//Property[@name="Type"]/docs/*' />
public IniType Type
{
get { return iniType; }
}
/// <include file='IniReader.xml' path='//Property[@name="Comment"]/docs/*' />
public string Comment
{
get { return (hasComment) ? this.comment.ToString () : null; }
}
/// <include file='IniReader.xml' path='//Property[@name="LineNumber"]/docs/*' />
public int LineNumber
{
get { return lineNumber; }
}
/// <include file='IniReader.xml' path='//Property[@name="LinePosition"]/docs/*' />
public int LinePosition
{
get { return column; }
}
/// <include file='IniReader.xml' path='//Property[@name="IgnoreComments"]/docs/*' />
public bool IgnoreComments
{
get { return ignoreComments; }
set { ignoreComments = value; }
}
/// <include file='IniReader.xml' path='//Property[@name="ReadState"]/docs/*' />
public IniReadState ReadState
{
get { return readState; }
}
/// <include file='IniReader.xml' path='//Property[@name="LineContinuation"]/docs/*' />
public bool LineContinuation
{
get { return lineContinuation; }
set { lineContinuation = value; }
}
/// <include file='IniReader.xml' path='//Property[@name="AcceptCommentAfterKey"]/docs/*' />
public bool AcceptCommentAfterKey
{
get { return acceptCommentAfterKey; }
set { acceptCommentAfterKey = value; }
}
/// <include file='IniReader.xml' path='//Property[@name="AcceptNoAssignmentOperator"]/docs/*' />
public bool AcceptNoAssignmentOperator
{
get { return acceptNoAssignmentOperator; }
set { acceptNoAssignmentOperator = value; }
}
/// <include file='IniReader.xml' path='//Property[@name="ConsumeAllKeyText"]/docs/*' />
public bool ConsumeAllKeyText
{
get { return consumeAllKeyText; }
set { consumeAllKeyText = value; }
}
#endregion
#region Constructors
/// <include file='IniReader.xml' path='//Constructor[@name="ConstructorPath"]/docs/*' />
public IniReader (string filePath)
{
textReader = new StreamReader (filePath);
}
/// <include file='IniReader.xml' path='//Constructor[@name="ConstructorTextReader"]/docs/*' />
public IniReader (TextReader reader)
{
textReader = reader;
}
/// <include file='IniReader.xml' path='//Constructor[@name="ConstructorStream"]/docs/*' />
public IniReader (Stream stream)
: this (new StreamReader (stream))
{
}
#endregion
#region Public methods
/// <include file='IniReader.xml' path='//Method[@name="Read"]/docs/*' />
public bool Read ()
{
bool result = false;
if (readState != IniReadState.EndOfFile
|| readState != IniReadState.Closed) {
readState = IniReadState.Interactive;
result = ReadNext ();
}
return result;
}
/// <include file='IniReader.xml' path='//Method[@name="MoveToNextSection"]/docs/*' />
public bool MoveToNextSection ()
{
bool result = false;
while (true)
{
result = Read ();
if (iniType == IniType.Section || !result) {
break;
}
}
return result;
}
/// <include file='IniReader.xml' path='//Method[@name="MoveToNextKey"]/docs/*' />
public bool MoveToNextKey ()
{
bool result = false;
while (true)
{
result = Read ();
if (iniType == IniType.Section) {
result = false;
break;
}
if (iniType == IniType.Key || !result) {
break;
}
}
return result;
}
/// <include file='IniReader.xml' path='//Method[@name="Close"]/docs/*' />
public void Close ()
{
Reset ();
readState = IniReadState.Closed;
if (textReader != null) {
textReader.Close ();
}
}
/// <include file='IniReader.xml' path='//Method[@name="Dispose"]/docs/*' />
public void Dispose ()
{
Dispose (true);
}
/// <include file='IniReader.xml' path='//Method[@name="GetCommentDelimiters"]/docs/*' />
public char[] GetCommentDelimiters ()
{
char[] result = new char[commentDelimiters.Length];
Array.Copy (commentDelimiters, 0, result, 0, commentDelimiters.Length);
return result;
}
/// <include file='IniReader.xml' path='//Method[@name="SetCommentDelimiters"]/docs/*' />
public void SetCommentDelimiters (char[] delimiters)
{
if (delimiters.Length < 1) {
throw new ArgumentException ("Must supply at least one delimiter");
}
commentDelimiters = delimiters;
}
/// <include file='IniReader.xml' path='//Method[@name="GetAssignDelimiters"]/docs/*' />
public char[] GetAssignDelimiters ()
{
char[] result = new char[assignDelimiters.Length];
Array.Copy (assignDelimiters, 0, result, 0, assignDelimiters.Length);
return result;
}
/// <include file='IniReader.xml' path='//Method[@name="SetAssignDelimiters"]/docs/*' />
public void SetAssignDelimiters (char[] delimiters)
{
if (delimiters.Length < 1) {
throw new ArgumentException ("Must supply at least one delimiter");
}
assignDelimiters = delimiters;
}
#endregion
#region Protected methods
/// <include file='IniReader.xml' path='//Method[@name="DisposeBoolean"]/docs/*' />
protected virtual void Dispose (bool disposing)
{
if (!disposed) {
textReader.Close ();
disposed = true;
if (disposing) {
GC.SuppressFinalize (this);
}
}
}
#endregion
#region Private methods
/// <summary>
/// Destructor.
/// </summary>
~IniReader ()
{
Dispose (false);
}
/// <summary>
/// Resets all of the current INI line data.
/// </summary>
private void Reset ()
{
this.name.Remove (0, this.name.Length);
this.value.Remove (0, this.value.Length);
this.comment.Remove (0, this.comment.Length);
iniType = IniType.Empty;
hasComment = false;
}
/// <summary>
/// Reads the next INI line item.
/// </summary>
private bool ReadNext ()
{
bool result = true;
int ch = PeekChar ();
Reset ();
if (IsComment (ch)) {
iniType = IniType.Empty;
ReadChar (); // consume comment character
ReadComment ();
return result;
}
switch (ch)
{
case ' ':
case '\t':
case '\r':
SkipWhitespace ();
ReadNext ();
break;
case '\n':
ReadChar ();
break;
case '[':
ReadSection ();
break;
case -1:
readState = IniReadState.EndOfFile;
result = false;
break;
default:
ReadKey ();
break;
}
return result;
}
/// <summary>
/// Reads a comment. Must start after the comment delimiter.
/// </summary>
private void ReadComment ()
{
int ch = -1;
SkipWhitespace ();
hasComment = true;
do
{
ch = ReadChar ();
this.comment.Append ((char)ch);
} while (!EndOfLine (ch));
RemoveTrailingWhitespace (this.comment);
}
/// <summary>
/// Removes trailing whitespace from a StringBuilder.
/// </summary>
private void RemoveTrailingWhitespace (StringBuilder builder)
{
string temp = builder.ToString ();
builder.Remove (0, builder.Length);
builder.Append (temp.TrimEnd (null));
}
/// <summary>
/// Reads a key.
/// </summary>
private void ReadKey ()
{
int ch = -1;
iniType = IniType.Key;
while (true)
{
ch = PeekChar ();
if (IsAssign (ch)) {
ReadChar ();
break;
}
if (EndOfLine (ch)) {
if (acceptNoAssignmentOperator) {
break;
}
throw new IniException (this,
String.Format ("Expected assignment operator ({0})",
assignDelimiters[0]));
}
this.name.Append ((char)ReadChar ());
}
ReadKeyValue ();
SearchForComment ();
RemoveTrailingWhitespace (this.name);
}
/// <summary>
/// Reads the value of a key.
/// </summary>
private void ReadKeyValue ()
{
int ch = -1;
bool foundQuote = false;
int characters = 0;
SkipWhitespace ();
while (true)
{
ch = PeekChar ();
if (!IsWhitespace (ch)) {
characters++;
}
if (!this.ConsumeAllKeyText && ch == '"') {
ReadChar ();
if (!foundQuote && characters == 1) {
foundQuote = true;
continue;
} else {
break;
}
}
if (foundQuote && EndOfLine (ch)) {
throw new IniException (this, "Expected closing quote (\")");
}
// Handle line continuation
if (lineContinuation && ch == '\\')
{
StringBuilder buffer = new StringBuilder ();
buffer.Append ((char)ReadChar ()); // append '\'
while (PeekChar () != '\n' && IsWhitespace (PeekChar ()))
{
if (PeekChar () != '\r') {
buffer.Append ((char)ReadChar ());
} else {
ReadChar (); // consume '\r'
}
}
if (PeekChar () == '\n') {
// continue reading key value on next line
ReadChar ();
continue;
} else {
// Replace consumed characters
this.value.Append (buffer.ToString ());
}
}
if (!this.ConsumeAllKeyText) {
// If accepting comments then don't consume as key value
if (acceptCommentAfterKey && IsComment (ch) && !foundQuote) {
break;
}
}
// Always break at end of line
if (EndOfLine (ch)) {
break;
}
this.value.Append ((char)ReadChar ());
}
if (!foundQuote) {
RemoveTrailingWhitespace (this.value);
}
}
/// <summary>
/// Reads an INI section.
/// </summary>
private void ReadSection ()
{
int ch = -1;
iniType = IniType.Section;
ch = ReadChar (); // consume "["
while (true)
{
ch = PeekChar ();
if (ch == ']') {
break;
}
if (EndOfLine (ch)) {
throw new IniException (this, "Expected section end (])");
}
this.name.Append ((char)ReadChar ());
}
ConsumeToEnd (); // all after '[' is garbage
RemoveTrailingWhitespace (this.name);
}
/// <summary>
/// Looks for a comment.
/// </summary>
private void SearchForComment ()
{
int ch = ReadChar ();
while (!EndOfLine (ch))
{
if (IsComment (ch)) {
if (ignoreComments) {
ConsumeToEnd ();
} else {
ReadComment ();
}
break;
}
ch = ReadChar ();
}
}
/// <summary>
/// Consumes all data until the end of a line.
/// </summary>
private void ConsumeToEnd ()
{
int ch = -1;
do
{
ch = ReadChar ();
} while (!EndOfLine (ch));
}
/// <summary>
/// Returns and consumes the next character from the stream.
/// </summary>
private int ReadChar ()
{
int result = textReader.Read ();
if (result == '\n') {
lineNumber++;
column = 1;
} else {
column++;
}
return result;
}
/// <summary>
/// Returns the next upcoming character from the stream.
/// </summary>
private int PeekChar ()
{
return textReader.Peek ();
}
/// <summary>
/// Returns true if a comment character is found.
/// </summary>
private bool IsComment (int ch)
{
return HasCharacter (commentDelimiters, ch);
}
/// <summary>
/// Returns true if character is an assign character.
/// </summary>
private bool IsAssign (int ch)
{
return HasCharacter (assignDelimiters, ch);
}
/// <summary>
/// Returns true if the character is found in the given array.
/// </summary>
private bool HasCharacter (char[] characters, int ch)
{
bool result = false;
for (int i = 0; i < characters.Length; i++)
{
if (ch == characters[i])
{
result = true;
break;
}
}
return result;
}
/// <summary>
/// Returns true if a value is whitespace.
/// </summary>
private bool IsWhitespace (int ch)
{
return ch == 0x20 || ch == 0x9 || ch == 0xD || ch == 0xA;
}
/// <summary>
/// Skips all whitespace.
/// </summary>
private void SkipWhitespace ()
{
while (IsWhitespace (PeekChar ()))
{
if (EndOfLine (PeekChar ())) {
break;
}
ReadChar ();
}
}
/// <summary>
/// Returns true if an end of line is found. End of line
/// includes both an end of line or end of file.
/// </summary>
private bool EndOfLine (int ch)
{
return (ch == '\n' || ch == -1);
}
#endregion
}
}