Commit b7799fc9 authored by Brickner_cp's avatar Brickner_cp

IPv6

parent 14169c2a
......@@ -73,10 +73,10 @@ namespace PcapDotNet.Packets.Test
// Ethernet
Assert.AreEqual(packet.Length - EthernetDatagram.HeaderLengthValue, packet.Ethernet.PayloadLength, "PayloadLength");
// Assert.AreEqual(ethernetLayer, packet.Ethernet.ExtractLayer(), "Ethernet Layer");
Assert.AreEqual(ethernetLayer, packet.Ethernet.ExtractLayer(), "Ethernet Layer");
// IpV6
// Assert.AreEqual(ipV6Layer, packet.Ethernet.IpV6.ExtractLayer(), "IP Layer");
Assert.AreEqual(ipV6Layer, packet.Ethernet.IpV6.ExtractLayer(), "IP Layer");
/*
if (packet.Ethernet.IpV6.NextHeader == IpV4Protocol.Tcp)
Assert.IsInstanceOfType(packet.Ethernet.IpV6.Transport, typeof(TcpDatagram));
......
......@@ -45,15 +45,18 @@ namespace PcapDotNet.Packets.TestUtils
IpV6ExtensionHeader[] headers = new IpV6ExtensionHeader[count];
for (int i = headers.Length - 1; i >= 0; --i)
{
headers[i] = random.NextIpV6ExtensionHeader(nextHeader);
headers[i] = random.NextIpV6ExtensionHeader(nextHeader, i == headers.Length - 1);
nextHeader = headers[i].Protocol;
}
return new IpV6ExtensionHeaders(headers);
}
public static IpV6ExtensionHeader NextIpV6ExtensionHeader(this Random random, IpV4Protocol nextHeader)
public static IpV6ExtensionHeader NextIpV6ExtensionHeader(this Random random, IpV4Protocol nextHeader, bool isEncapsulatingSecurityPayloadPossible)
{
IpV4Protocol extensionHeaderType = random.NextValue(IpV6ExtensionHeader.ExtensionHeaders);
IpV4Protocol extensionHeaderType =
random.NextValue(
IpV6ExtensionHeader.ExtensionHeaders.Where(extensionHeader => isEncapsulatingSecurityPayloadPossible ||
extensionHeader != IpV4Protocol.EncapsulatingSecurityPayload).ToList());
switch (extensionHeaderType)
{
case IpV4Protocol.IpV6HopByHopOption: // 0
......@@ -84,7 +87,8 @@ namespace PcapDotNet.Packets.TestUtils
random.NextBool(), random.NextUInt());
case IpV4Protocol.IpV6Opts: // 60
return new IpV6ExtensionHeaderDestinationOptions(nextHeader, random.NextIpV6Options());
IpV6Options options = random.NextIpV6Options();
return new IpV6ExtensionHeaderDestinationOptions(nextHeader, options);
case IpV4Protocol.MobilityHeader: // 135
return random.NextIpV6ExtensionHeaderMobility(nextHeader);
......
......@@ -105,16 +105,9 @@ namespace PcapDotNet.Packets.Ip
IsValid = isValid;
if (length.HasValue)
{
BytesLength = length.Value;
}
else
{
BytesLength = SumBytesLength(OptionsCollection);
if (BytesLength % 4 != 0)
BytesLength = (BytesLength / 4 + 1) * 4;
}
BytesLength = CalculateBytesLength(SumBytesLength(OptionsCollection));
}
internal static int SumBytesLength(IEnumerable<T> options)
......@@ -122,6 +115,8 @@ namespace PcapDotNet.Packets.Ip
return options.Sum(option => option.Length);
}
internal abstract int CalculateBytesLength(int optionsLength);
private readonly ReadOnlyCollection<T> _options;
}
}
\ No newline at end of file
......@@ -18,7 +18,14 @@ namespace PcapDotNet.Packets.Ip
if (BytesLength > maximumBytesLength)
throw new ArgumentException("given options take " + BytesLength + " bytes and maximum number of bytes for options is " + maximumBytesLength, "options");
}
internal override sealed int CalculateBytesLength(int optionsLength)
{
if (optionsLength % 4 == 0)
return optionsLength;
return (optionsLength / 4 + 1) * 4;
}
private V4Options(Tuple<IList<T>, bool> optionsAndIsValid, int? length)
: this(optionsAndIsValid.Item1, optionsAndIsValid.Item2, length)
{
......
......@@ -44,13 +44,13 @@ namespace PcapDotNet.Packets.IpV6
private static class Offset
{
public const int Version = 0;
public const int TrafficClass = 0;
public const int FlowLabel = 1;
public const int PayloadLength = 4;
public const int NextHeader = 6;
public const int HopLimit = 7;
public const int SourceAddress = 8;
public const int DestinationAddress = 24;
public const int TrafficClass = Version;
public const int FlowLabel = TrafficClass + 1;
public const int PayloadLength = FlowLabel + 3;
public const int NextHeader = PayloadLength + sizeof(ushort);
public const int HopLimit = NextHeader + sizeof(byte);
public const int SourceAddress = HopLimit + sizeof(byte);
public const int DestinationAddress = SourceAddress + IpV6Address.SizeOf;
}
private static class Mask
......@@ -172,6 +172,7 @@ namespace PcapDotNet.Packets.IpV6
return;
}
_extensionHeaders = new IpV6ExtensionHeaders(Subsegment(HeaderLength, RealPayloadLength), NextHeader);
_isValid = _isValid && _extensionHeaders.IsValid;
/*
int extendedHeaderLength = HeaderLength;
IpV4Protocol? nextHeader = NextHeader;
......@@ -236,7 +237,7 @@ namespace PcapDotNet.Packets.IpV6
byte trafficClass, int flowLabel, ushort payloadLength, IpV4Protocol nextHeader, byte hopLimit,
IpV6Address source, IpV6Address currentDestination, IpV6ExtensionHeaders extensionHeaders)
{
buffer.Write(offset + Offset.Version, (uint)(((((DefaultVersion << Shift.Version) << 8) | trafficClass) << 16) | flowLabel), Endianity.Big);
buffer.Write(offset + Offset.Version, (uint)((((DefaultVersion << 8) | trafficClass) << 20) | flowLabel), Endianity.Big);
buffer.Write(offset + Offset.PayloadLength, payloadLength, Endianity.Big);
buffer.Write(offset + Offset.NextHeader, (byte)nextHeader);
buffer.Write(offset + Offset.HopLimit, hopLimit);
......@@ -253,7 +254,6 @@ namespace PcapDotNet.Packets.IpV6
}
private IpV6ExtensionHeaders _extensionHeaders;
private int _extensionHeadersLength;
private bool _isValid;
}
}
......@@ -33,6 +33,14 @@ namespace PcapDotNet.Packets.IpV6
public const int MinimumLength = 8;
public override sealed bool Equals(IpV6ExtensionHeader other)
{
return other != null &&
Protocol == other.Protocol && NextHeader == other.NextHeader && EqualsData(other);
}
internal abstract bool EqualsData(IpV6ExtensionHeader other);
internal IpV6ExtensionHeaderStandard(IpV4Protocol nextHeader)
: base(nextHeader)
{
......@@ -49,7 +57,7 @@ namespace PcapDotNet.Packets.IpV6
internal abstract void WriteData(byte[] buffer, int offset);
public override sealed int Length { get { return MinimumLength + DataLength; } }
public override sealed int Length { get { return Offset.Data + DataLength; } }
internal abstract int DataLength { get; }
internal static bool IsStandard(IpV4Protocol nextHeader)
{
......@@ -132,7 +140,7 @@ namespace PcapDotNet.Packets.IpV6
/// <summary>
/// RFC 2460.
/// </summary>
public abstract class IpV6ExtensionHeader
public abstract class IpV6ExtensionHeader : IEquatable<IpV6ExtensionHeader>
{
public abstract IpV4Protocol Protocol { get; }
......@@ -145,6 +153,15 @@ namespace PcapDotNet.Packets.IpV6
get { return _extensionHeaders; }
}
public abstract bool IsValid { get; }
public override sealed bool Equals(object obj)
{
return Equals(obj as IpV6ExtensionHeader);
}
public abstract bool Equals(IpV6ExtensionHeader other);
internal static bool IsExtensionHeader(IpV4Protocol nextHeader)
{
if (IpV6ExtensionHeaderStandard.IsStandard(nextHeader))
......@@ -301,7 +318,7 @@ namespace PcapDotNet.Packets.IpV6
data = data.Subsegment(numBytesRead, data.Length - numBytesRead);
}
Headers = headers.AsReadOnly();
IsValid = (!nextHeader.HasValue || !IpV6ExtensionHeader.IsExtensionHeader(nextHeader.Value));
IsValid = (!nextHeader.HasValue || !IpV6ExtensionHeader.IsExtensionHeader(nextHeader.Value)) && headers.All(header => header.IsValid);
}
private static readonly IpV6ExtensionHeaders _empty = new IpV6ExtensionHeaders();
......
......@@ -20,7 +20,7 @@ namespace PcapDotNet.Packets.IpV6
/// +-----+--------------------------------------+
/// </pre>
/// </summary>
public class IpV6ExtensionHeaderAuthentication : IpV6ExtensionHeader
public class IpV6ExtensionHeaderAuthentication : IpV6ExtensionHeader, IEquatable<IpV6ExtensionHeaderAuthentication>
{
private static class Offset
{
......@@ -100,7 +100,7 @@ namespace PcapDotNet.Packets.IpV6
}
IpV4Protocol nextHeader = (IpV4Protocol)extensionHeaderData[Offset.NextHeader];
int payloadLength = (extensionHeaderData[Offset.PayloadLength] + 2) * 4;
if (extensionHeaderData.Length < Offset.AuthenticationData + payloadLength)
if (extensionHeaderData.Length < payloadLength || payloadLength < MinimumLength)
{
numBytesRead = 0;
return null;
......@@ -137,6 +137,23 @@ namespace PcapDotNet.Packets.IpV6
get { return MinimumLength + AuthenticationData.Length; }
}
public override bool IsValid
{
get { return true; }
}
public override bool Equals(IpV6ExtensionHeader other)
{
return Equals(other as IpV6ExtensionHeaderAuthentication);
}
public bool Equals(IpV6ExtensionHeaderAuthentication other)
{
return other != null &&
NextHeader == other.NextHeader && SecurityParametersIndex == other.SecurityParametersIndex && SequenceNumber == other.SequenceNumber &&
AuthenticationData.Equals(other.AuthenticationData);
}
internal override void Write(byte[] buffer, ref int offset)
{
buffer.Write(offset + Offset.NextHeader, (byte)NextHeader);
......
using System;
using PcapDotNet.Packets.IpV4;
namespace PcapDotNet.Packets.IpV6
......
using System;
using PcapDotNet.Packets.IpV4;
namespace PcapDotNet.Packets.IpV6
......@@ -45,7 +46,7 @@ namespace PcapDotNet.Packets.IpV6
/// +-----+--------------------------+
/// </pre>
/// </summary>
public class IpV6ExtensionHeaderEncapsulatingSecurityPayload : IpV6ExtensionHeader
public sealed class IpV6ExtensionHeaderEncapsulatingSecurityPayload : IpV6ExtensionHeader, IEquatable<IpV6ExtensionHeaderEncapsulatingSecurityPayload>
{
private static class Offset
{
......@@ -74,6 +75,23 @@ namespace PcapDotNet.Packets.IpV6
get { return MinimumLength + EncryptedDataAndAuthenticationData.Length; }
}
public override bool IsValid
{
get { return true; }
}
public override bool Equals(IpV6ExtensionHeader other)
{
return Equals(other as IpV6ExtensionHeaderEncapsulatingSecurityPayload);
}
public bool Equals(IpV6ExtensionHeaderEncapsulatingSecurityPayload other)
{
return other != null &&
SecurityParametersIndex == other.SecurityParametersIndex && SequenceNumber == other.SequenceNumber &&
EncryptedDataAndAuthenticationData.Equals(other.EncryptedDataAndAuthenticationData);
}
/// <summary>
/// <para>
/// The SPI is an arbitrary 32-bit value that, in combination with the destination IP address and security protocol (ESP),
......
using System;
using PcapDotNet.Packets.IpV4;
namespace PcapDotNet.Packets.IpV6
......@@ -19,11 +20,21 @@ namespace PcapDotNet.Packets.IpV6
{
public IpV6Options Options { get; private set; }
public override sealed bool IsValid
{
get { return Options.IsValid; }
}
internal override sealed int DataLength
{
get { return Options.BytesLength; }
}
internal override sealed bool EqualsData(IpV6ExtensionHeader other)
{
return EqualsData(other as IpV6ExtensionHeaderOptions);
}
internal override void WriteData(byte[] buffer, int offset)
{
Options.Write(buffer, offset);
......@@ -32,7 +43,15 @@ namespace PcapDotNet.Packets.IpV6
internal IpV6ExtensionHeaderOptions(IpV4Protocol nextHeader, IpV6Options options)
: base(nextHeader)
{
if (options.BytesLength % 8 != 6)
throw new ArgumentException("Options length in bytes must be an integral product of 8 + 6.", "options");
Options = options;
}
private bool EqualsData(IpV6ExtensionHeaderOptions other)
{
return other != null &&
Options.Equals(other.Options);
}
}
}
\ No newline at end of file
......@@ -26,7 +26,10 @@ namespace PcapDotNet.Packets.IpV6
public const int DataMinimumLength = DataOffset.TypeSpecificData;
internal abstract int RoutingDataLength { get; }
public override bool IsValid
{
get { return true; }
}
/// <summary>
/// Identifier of a particular Routing header variant.
......@@ -54,6 +57,15 @@ namespace PcapDotNet.Packets.IpV6
get { return DataMinimumLength + RoutingDataLength; }
}
internal abstract int RoutingDataLength { get; }
internal override sealed bool EqualsData(IpV6ExtensionHeader other)
{
return EqualsData(other as IpV6ExtensionHeaderRouting);
}
internal abstract bool EqualsRoutingData(IpV6ExtensionHeaderRouting other);
internal static IpV6ExtensionHeaderRouting ParseData(IpV4Protocol nextHeader, DataSegment data)
{
if (data.Length < DataMinimumLength)
......@@ -89,5 +101,11 @@ namespace PcapDotNet.Packets.IpV6
}
internal abstract void WriteRoutingData(byte[] buffer, int offset);
private bool EqualsData(IpV6ExtensionHeaderRouting other)
{
return other != null &&
RoutingType == other.RoutingType && SegmentsLeft == other.SegmentsLeft && EqualsRoutingData(other);
}
}
}
\ No newline at end of file
......@@ -19,7 +19,7 @@ namespace PcapDotNet.Packets.IpV6
/// +-----+----------------------------------------------------------------------+
/// </pre>
/// </summary>
public class IpV6ExtensionHeaderRoutingHomeAddress : IpV6ExtensionHeaderRouting
public sealed class IpV6ExtensionHeaderRoutingHomeAddress : IpV6ExtensionHeaderRouting
{
private static class RoutingDataOffset
{
......@@ -58,9 +58,20 @@ namespace PcapDotNet.Packets.IpV6
return new IpV6ExtensionHeaderRoutingHomeAddress(nextHeader, segmentsLeft, homeAddress);
}
internal override bool EqualsRoutingData(IpV6ExtensionHeaderRouting other)
{
return EqualsRoutingData(other as IpV6ExtensionHeaderRoutingHomeAddress);
}
internal override void WriteRoutingData(byte[] buffer, int offset)
{
buffer.Write(offset + RoutingDataOffset.HomeAddress, HomeAddress, Endianity.Big);
}
private bool EqualsRoutingData(IpV6ExtensionHeaderRoutingHomeAddress other)
{
return other != null &&
HomeAddress.Equals(other.HomeAddress);
}
}
}
\ No newline at end of file
......@@ -34,7 +34,7 @@ namespace PcapDotNet.Packets.IpV6
/// +-----+------------------------------------------------------------------------+
/// </pre>
/// </summary>
public class IpV6ExtensionHeaderRoutingRpl : IpV6ExtensionHeaderRouting
public sealed class IpV6ExtensionHeaderRoutingRpl : IpV6ExtensionHeaderRouting
{
private static class RoutingDataOffset
{
......@@ -170,6 +170,11 @@ namespace PcapDotNet.Packets.IpV6
addresses);
}
internal override bool EqualsRoutingData(IpV6ExtensionHeaderRouting other)
{
return EqualsRoutingData(other as IpV6ExtensionHeaderRoutingRpl);
}
internal override void WriteRoutingData(byte[] buffer, int offset)
{
buffer.Write(offset + RoutingDataOffset.CommonPrefixLengthForNonLastAddresses,
......@@ -189,5 +194,13 @@ namespace PcapDotNet.Packets.IpV6
addressBytes.SubSegment(CommonPrefixLengthForLastAddress, IpV6Address.SizeOf - CommonPrefixLengthForLastAddress).Write(buffer, ref addressOffset);
}
}
private bool EqualsRoutingData(IpV6ExtensionHeaderRoutingRpl other)
{
return other != null &&
CommonPrefixLengthForNonLastAddresses == other.CommonPrefixLengthForNonLastAddresses &&
CommonPrefixLengthForLastAddress == other.CommonPrefixLengthForLastAddress && PadSize == other.PadSize &&
Addresses.SequenceEqual(other.Addresses);
}
}
}
\ No newline at end of file
using System.Collections.ObjectModel;
using System.Linq;
using PcapDotNet.Base;
using PcapDotNet.Packets.IpV4;
......@@ -35,7 +36,7 @@ namespace PcapDotNet.Packets.IpV6
/// +-----+----------------------------------------------------------------------+
/// </pre>
/// </summary>
public class IpV6ExtensionHeaderRoutingSourceRoute : IpV6ExtensionHeaderRouting
public sealed class IpV6ExtensionHeaderRoutingSourceRoute : IpV6ExtensionHeaderRouting
{
private static class RoutingDataOffset
{
......@@ -78,10 +79,21 @@ namespace PcapDotNet.Packets.IpV6
return new IpV6ExtensionHeaderRoutingSourceRoute(nextHeader, segmentsLeft, addresses);
}
internal override bool EqualsRoutingData(IpV6ExtensionHeaderRouting other)
{
return EqualsRoutingData(other as IpV6ExtensionHeaderRoutingSourceRoute);
}
internal override void WriteRoutingData(byte[] buffer, int offset)
{
for (int i = 0; i != Addresses.Count; ++i)
buffer.Write(offset + RoutingDataOffset.Addresses + i * IpV6Address.SizeOf, Addresses[i], Endianity.Big);
}
private bool EqualsRoutingData(IpV6ExtensionHeaderRoutingSourceRoute other)
{
return other != null &&
Addresses.SequenceEqual(other.Addresses);
}
}
}
\ No newline at end of file
......@@ -101,10 +101,12 @@ namespace PcapDotNet.Packets.IpV6
nextHeader = ipV4NextLayer.PreviousLayerProtocol;
}
else
{
nextHeader = NextHeader.Value;
}
IpV6Datagram.WriteHeader(buffer, offset,
TrafficClass, FlowLabel, (ushort)payloadLength, nextHeader, HopLimit, Source, CurrentDestination, ExtensionHeaders);
TrafficClass, FlowLabel, (ushort)(payloadLength + ExtensionHeaders.Sum(header => header.Length)), nextHeader, HopLimit, Source, CurrentDestination, ExtensionHeaders);
}
/// <summary>
......
......@@ -42,7 +42,7 @@ namespace PcapDotNet.Packets.IpV6
public IpV6OptionRoutingProtocolLowPowerAndLossyNetworks(bool down, bool rankError, bool forwardingError, byte rplInstanceId, ushort senderRank,
DataSegment subTlvs)
: base(IpV6OptionType.EndpointIdentification)
: base(IpV6OptionType.RplOption)
{
Down = down;
RankError = rankError;
......
......@@ -3938,14 +3938,14 @@ namespace PcapDotNet.Packets.IpV6
/// <summary>
/// RFC 6089.
/// </summary>
public class IpV6FlowIdentificationSubOptions : Options<IpV6FlowIdentificationSubOption>
public class IpV6FlowIdentificationSubOptions : V6Options<IpV6FlowIdentificationSubOption>
{
/// <summary>
/// Creates options from a list of options.
/// </summary>
/// <param name="options">The list of options.</param>
public IpV6FlowIdentificationSubOptions(IList<IpV6FlowIdentificationSubOption> options)
: base(options, true, null)
: base(options, true)
{
}
......@@ -3964,7 +3964,7 @@ namespace PcapDotNet.Packets.IpV6
}
private IpV6FlowIdentificationSubOptions(Tuple<IList<IpV6FlowIdentificationSubOption>, bool> optionsAndIsValid)
: base(optionsAndIsValid.Item1, optionsAndIsValid.Item2, null)
: base(optionsAndIsValid.Item1, optionsAndIsValid.Item2)
{
}
......@@ -4955,14 +4955,14 @@ namespace PcapDotNet.Packets.IpV6
/// <summary>
/// RFC 6757.
/// </summary>
public class IpV6AccessNetworkIdentifierSubOptions : Options<IpV6AccessNetworkIdentifierSubOption>
public class IpV6AccessNetworkIdentifierSubOptions : V6Options<IpV6AccessNetworkIdentifierSubOption>
{
/// <summary>
/// Creates options from a list of options.
/// </summary>
/// <param name="options">The list of options.</param>
public IpV6AccessNetworkIdentifierSubOptions(IList<IpV6AccessNetworkIdentifierSubOption> options)
: base(options, true, null)
: base(options, true)
{
}
......@@ -4981,7 +4981,7 @@ namespace PcapDotNet.Packets.IpV6
}
private IpV6AccessNetworkIdentifierSubOptions(Tuple<IList<IpV6AccessNetworkIdentifierSubOption>, bool> optionsAndIsValid)
: base(optionsAndIsValid.Item1, optionsAndIsValid.Item2, null)
: base(optionsAndIsValid.Item1, optionsAndIsValid.Item2)
{
}
......
......@@ -12,22 +12,38 @@ namespace PcapDotNet.Packets.IpV6
IpV6Option CreateInstance(DataSegment data);
}
public class IpV6Options : Options<IpV6Option>
public sealed class IpV6Options : Options<IpV6Option>
{
public IpV6Options(IList<IpV6Option> options)
: base(options, true, null)
: base(AddPadding(options), true, null)
{
}
public IpV6Options(IEnumerable<IpV6Option> options)
: this(options.ToList())
{
}
public IpV6Options(DataSegment data) : this(Read(data))
public IpV6Options(DataSegment data)
: this(Read(data))
{
}
private IpV6Options(Tuple<IList<IpV6Option>, bool> optionsAndIsValid) : base(optionsAndIsValid.Item1, optionsAndIsValid.Item2, null)
private IpV6Options(Tuple<IList<IpV6Option>, bool> optionsAndIsValid)
: base(AddPadding(optionsAndIsValid.Item1), optionsAndIsValid.Item2, null)
{
}
private static IList<IpV6Option> AddPadding(IList<IpV6Option> options)
{
int optionsLength = options.Sum(option => option.Length);
if (optionsLength % 8 == 6)
return options;
int paddingLength = (14 - optionsLength % 8) % 8;
return options.Concat(paddingLength == 1 ? (IpV6Option)new IpV6OptionPad1() : new IpV6OptionPadN(paddingLength - 2)).ToArray();
}
public static Tuple<IList<IpV6Option>, bool> Read(DataSegment data)
{
int offset = 0;
......@@ -68,6 +84,11 @@ namespace PcapDotNet.Packets.IpV6
return new Tuple<IList<IpV6Option>, bool>(options, isValid);
}
internal override int CalculateBytesLength(int optionsLength)
{
return optionsLength;
}
private static IpV6Option CreateOption(IpV6OptionType optionType, DataSegment data)
{
IIpV6OptionComplexFactory factory;
......@@ -102,14 +123,27 @@ namespace PcapDotNet.Packets.IpV6
}
}
public class IpV6MobilityOptions : Options<IpV6MobilityOption>
public abstract class V6Options<T> : Options<T> where T : Option
{
public V6Options(IList<T> options, bool isValid)
: base(options, isValid, null)
{
}
internal override sealed int CalculateBytesLength(int optionsLength)
{
return optionsLength;
}
}
public class IpV6MobilityOptions : V6Options<IpV6MobilityOption>
{
/// <summary>
/// Creates options from a list of options.
/// </summary>
/// <param name="options">The list of options.</param>
public IpV6MobilityOptions(IList<IpV6MobilityOption> options)
: base(options, true, null)
: base(options, true)
{
}
......@@ -128,7 +162,7 @@ namespace PcapDotNet.Packets.IpV6
}
private IpV6MobilityOptions(Tuple<IList<IpV6MobilityOption>, bool> optionsAndIsValid)
: base(optionsAndIsValid.Item1, optionsAndIsValid.Item2, null)
: base(optionsAndIsValid.Item1, optionsAndIsValid.Item2)
{
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment