mirror of
https://github.com/ReneLergner/WPinternals.git
synced 2026-06-14 03:16:40 +10:00
619 lines
24 KiB
C#
619 lines
24 KiB
C#
//
|
|
// Copyright (c) 2008-2011, Kenneth Bell
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
//
|
|
|
|
namespace DiscUtils.Ntfs
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Runtime.Serialization;
|
|
using System.Text;
|
|
|
|
/// <summary>
|
|
/// Class that checks NTFS file system integrity.
|
|
/// </summary>
|
|
/// <remarks>Poor relation of chkdsk/fsck.</remarks>
|
|
public sealed class NtfsFileSystemChecker : DiscFileSystemChecker
|
|
{
|
|
private Stream _target;
|
|
|
|
private NtfsContext _context;
|
|
private TextWriter _report;
|
|
private ReportLevels _reportLevels;
|
|
|
|
private ReportLevels _levelsDetected;
|
|
private ReportLevels _levelsConsideredFail = ReportLevels.Errors;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the NtfsFileSystemChecker class.
|
|
/// </summary>
|
|
/// <param name="diskData">The file system to check.</param>
|
|
public NtfsFileSystemChecker(Stream diskData)
|
|
{
|
|
SnapshotStream protectiveStream = new SnapshotStream(diskData, Ownership.None);
|
|
protectiveStream.Snapshot();
|
|
protectiveStream.Freeze();
|
|
_target = protectiveStream;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks the integrity of an NTFS file system held in a stream.
|
|
/// </summary>
|
|
/// <param name="reportOutput">A report on issues found.</param>
|
|
/// <param name="levels">The amount of detail to report.</param>
|
|
/// <returns><c>true</c> if the file system appears valid, else <c>false</c>.</returns>
|
|
public override bool Check(TextWriter reportOutput, ReportLevels levels)
|
|
{
|
|
_context = new NtfsContext();
|
|
_context.RawStream = _target;
|
|
_context.Options = new NtfsOptions();
|
|
|
|
_report = reportOutput;
|
|
_reportLevels = levels;
|
|
_levelsDetected = ReportLevels.None;
|
|
|
|
try
|
|
{
|
|
DoCheck();
|
|
}
|
|
catch (AbortException ae)
|
|
{
|
|
ReportError("File system check aborted: " + ae.ToString());
|
|
return false;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ReportError("File system check aborted with exception: " + e.ToString());
|
|
return false;
|
|
}
|
|
|
|
return (_levelsDetected & _levelsConsideredFail) == 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an object that can convert between clusters and files.
|
|
/// </summary>
|
|
/// <returns>The cluster map.</returns>
|
|
public ClusterMap BuildClusterMap()
|
|
{
|
|
_context = new NtfsContext();
|
|
_context.RawStream = _target;
|
|
_context.Options = new NtfsOptions();
|
|
|
|
_context.RawStream.Position = 0;
|
|
byte[] bytes = Utilities.ReadFully(_context.RawStream, 512);
|
|
|
|
_context.BiosParameterBlock = BiosParameterBlock.FromBytes(bytes, 0);
|
|
|
|
_context.Mft = new MasterFileTable(_context);
|
|
File mftFile = new File(_context, _context.Mft.GetBootstrapRecord());
|
|
_context.Mft.Initialize(mftFile);
|
|
return _context.Mft.GetClusterMap();
|
|
}
|
|
|
|
private static void Abort()
|
|
{
|
|
throw new AbortException();
|
|
}
|
|
|
|
private void DoCheck()
|
|
{
|
|
_context.RawStream.Position = 0;
|
|
byte[] bytes = Utilities.ReadFully(_context.RawStream, 512);
|
|
|
|
_context.BiosParameterBlock = BiosParameterBlock.FromBytes(bytes, 0);
|
|
|
|
//-----------------------------------------------------------------------
|
|
// MASTER FILE TABLE
|
|
//
|
|
|
|
// Bootstrap the Master File Table
|
|
_context.Mft = new MasterFileTable(_context);
|
|
File mftFile = new File(_context, _context.Mft.GetBootstrapRecord());
|
|
|
|
// Verify basic MFT records before initializing the Master File Table
|
|
PreVerifyMft(mftFile);
|
|
_context.Mft.Initialize(mftFile);
|
|
|
|
// Now the MFT is up and running, do more detailed analysis of it's contents - double-accounted clusters, etc
|
|
VerifyMft();
|
|
_context.Mft.Dump(_report, "INFO: ");
|
|
|
|
//-----------------------------------------------------------------------
|
|
// INDEXES
|
|
//
|
|
|
|
// Need UpperCase in order to verify some indexes (i.e. directories).
|
|
File ucFile = new File(_context, _context.Mft.GetRecord(MasterFileTable.UpCaseIndex, false));
|
|
_context.UpperCase = new UpperCase(ucFile);
|
|
|
|
SelfCheckIndexes();
|
|
|
|
//-----------------------------------------------------------------------
|
|
// DIRECTORIES
|
|
//
|
|
VerifyDirectories();
|
|
|
|
//-----------------------------------------------------------------------
|
|
// WELL KNOWN FILES
|
|
//
|
|
VerifyWellKnownFilesExist();
|
|
|
|
//-----------------------------------------------------------------------
|
|
// OBJECT IDS
|
|
//
|
|
VerifyObjectIds();
|
|
|
|
//-----------------------------------------------------------------------
|
|
// FINISHED
|
|
//
|
|
|
|
// Temporary...
|
|
using (NtfsFileSystem fs = new NtfsFileSystem(_context.RawStream))
|
|
{
|
|
if ((_reportLevels & ReportLevels.Information) != 0)
|
|
{
|
|
ReportDump(fs);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void VerifyWellKnownFilesExist()
|
|
{
|
|
Directory rootDir = new Directory(_context, _context.Mft.GetRecord(MasterFileTable.RootDirIndex, false));
|
|
|
|
DirectoryEntry extendDirEntry = rootDir.GetEntryByName("$Extend");
|
|
if (extendDirEntry == null)
|
|
{
|
|
ReportError("$Extend does not exist in root directory");
|
|
Abort();
|
|
}
|
|
|
|
Directory extendDir = new Directory(_context, _context.Mft.GetRecord(extendDirEntry.Reference));
|
|
|
|
DirectoryEntry objIdDirEntry = extendDir.GetEntryByName("$ObjId");
|
|
if (objIdDirEntry == null)
|
|
{
|
|
ReportError("$ObjId does not exist in $Extend directory");
|
|
Abort();
|
|
}
|
|
|
|
// Stash ObjectIds
|
|
_context.ObjectIds = new ObjectIds(new File(_context, _context.Mft.GetRecord(objIdDirEntry.Reference)));
|
|
|
|
DirectoryEntry sysVolInfDirEntry = rootDir.GetEntryByName("System Volume Information");
|
|
if (sysVolInfDirEntry == null)
|
|
{
|
|
ReportError("'System Volume Information' does not exist in root directory");
|
|
Abort();
|
|
}
|
|
////Directory sysVolInfDir = new Directory(_context, _context.Mft.GetRecord(sysVolInfDirEntry.Reference));
|
|
}
|
|
|
|
private void VerifyObjectIds()
|
|
{
|
|
foreach (FileRecord fr in _context.Mft.Records)
|
|
{
|
|
if (fr.BaseFile.Value != 0)
|
|
{
|
|
File f = new File(_context, fr);
|
|
foreach (var stream in f.AllStreams)
|
|
{
|
|
if (stream.AttributeType == AttributeType.ObjectId)
|
|
{
|
|
ObjectId objId = stream.GetContent<ObjectId>();
|
|
ObjectIdRecord objIdRec;
|
|
if (!_context.ObjectIds.TryGetValue(objId.Id, out objIdRec))
|
|
{
|
|
ReportError("ObjectId {0} for file {1} is not indexed", objId.Id, f.BestName);
|
|
}
|
|
else if (objIdRec.MftReference != f.MftReference)
|
|
{
|
|
ReportError("ObjectId {0} for file {1} points to {2}", objId.Id, f.BestName, objIdRec.MftReference);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var objIdRec in _context.ObjectIds.All)
|
|
{
|
|
if (_context.Mft.GetRecord(objIdRec.Value.MftReference) == null)
|
|
{
|
|
ReportError("ObjectId {0} refers to non-existant file {1}", objIdRec.Key, objIdRec.Value.MftReference);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void VerifyDirectories()
|
|
{
|
|
foreach (FileRecord fr in _context.Mft.Records)
|
|
{
|
|
if (fr.BaseFile.Value != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
File f = new File(_context, fr);
|
|
foreach (var stream in f.AllStreams)
|
|
{
|
|
if (stream.AttributeType == AttributeType.IndexRoot && stream.Name == "$I30")
|
|
{
|
|
IndexView<FileNameRecord, FileRecordReference> dir = new IndexView<FileNameRecord, FileRecordReference>(f.GetIndex("$I30"));
|
|
foreach (var entry in dir.Entries)
|
|
{
|
|
FileRecord refFile = _context.Mft.GetRecord(entry.Value);
|
|
|
|
// Make sure each referenced file actually exists...
|
|
if (refFile == null)
|
|
{
|
|
ReportError("Directory {0} references non-existent file {1}", f, entry.Key);
|
|
}
|
|
|
|
File referencedFile = new File(_context, refFile);
|
|
StandardInformation si = referencedFile.StandardInformation;
|
|
if (si.CreationTime != entry.Key.CreationTime || si.MftChangedTime != entry.Key.MftChangedTime
|
|
|| si.ModificationTime != entry.Key.ModificationTime)
|
|
{
|
|
ReportInfo("Directory entry {0} in {1} is out of date", entry.Key, f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SelfCheckIndexes()
|
|
{
|
|
foreach (FileRecord fr in _context.Mft.Records)
|
|
{
|
|
File f = new File(_context, fr);
|
|
foreach (var stream in f.AllStreams)
|
|
{
|
|
if (stream.AttributeType == AttributeType.IndexRoot)
|
|
{
|
|
SelfCheckIndex(f, stream.Name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SelfCheckIndex(File file, string name)
|
|
{
|
|
ReportInfo("About to self-check index {0} in file {1} (MFT:{2})", name, file.BestName, file.IndexInMft);
|
|
|
|
IndexRoot root = file.GetStream(AttributeType.IndexRoot, name).GetContent<IndexRoot>();
|
|
|
|
byte[] rootBuffer;
|
|
using (Stream s = file.OpenStream(AttributeType.IndexRoot, name, FileAccess.Read))
|
|
{
|
|
rootBuffer = Utilities.ReadFully(s, (int)s.Length);
|
|
}
|
|
|
|
Bitmap indexBitmap = null;
|
|
if (file.GetStream(AttributeType.Bitmap, name) != null)
|
|
{
|
|
indexBitmap = new Bitmap(file.OpenStream(AttributeType.Bitmap, name, FileAccess.Read), long.MaxValue);
|
|
}
|
|
|
|
if (!SelfCheckIndexNode(rootBuffer, IndexRoot.HeaderOffset, indexBitmap, root, file.BestName, name))
|
|
{
|
|
ReportError("Index {0} in file {1} (MFT:{2}) has corrupt IndexRoot attribute", name, file.BestName, file.IndexInMft);
|
|
}
|
|
else
|
|
{
|
|
ReportInfo("Self-check of index {0} in file {1} (MFT:{2}) complete", name, file.BestName, file.IndexInMft);
|
|
}
|
|
}
|
|
|
|
private bool SelfCheckIndexNode(byte[] buffer, int offset, Bitmap bitmap, IndexRoot root, string fileName, string indexName)
|
|
{
|
|
bool ok = true;
|
|
|
|
IndexHeader header = new IndexHeader(buffer, offset);
|
|
|
|
IndexEntry lastEntry = null;
|
|
|
|
IComparer<byte[]> collator = root.GetCollator(_context.UpperCase);
|
|
|
|
int pos = (int)header.OffsetToFirstEntry;
|
|
while (pos < header.TotalSizeOfEntries)
|
|
{
|
|
IndexEntry entry = new IndexEntry(indexName == "$I30");
|
|
entry.Read(buffer, offset + pos);
|
|
pos += entry.Size;
|
|
|
|
if ((entry.Flags & IndexEntryFlags.Node) != 0)
|
|
{
|
|
long bitmapIdx = entry.ChildrenVirtualCluster / Utilities.Ceil(root.IndexAllocationSize, _context.BiosParameterBlock.SectorsPerCluster * _context.BiosParameterBlock.BytesPerSector);
|
|
if (!bitmap.IsPresent(bitmapIdx))
|
|
{
|
|
ReportError("Index entry {0} is non-leaf, but child vcn {1} is not in bitmap at index {2}", Index.EntryAsString(entry, fileName, indexName), entry.ChildrenVirtualCluster, bitmapIdx);
|
|
}
|
|
}
|
|
|
|
if ((entry.Flags & IndexEntryFlags.End) != 0)
|
|
{
|
|
if (pos != header.TotalSizeOfEntries)
|
|
{
|
|
ReportError("Found END index entry {0}, but not at end of node", Index.EntryAsString(entry, fileName, indexName));
|
|
ok = false;
|
|
}
|
|
}
|
|
|
|
if (lastEntry != null && collator.Compare(lastEntry.KeyBuffer, entry.KeyBuffer) >= 0)
|
|
{
|
|
ReportError("Found entries out of order {0} was before {1}", Index.EntryAsString(lastEntry, fileName, indexName), Index.EntryAsString(entry, fileName, indexName));
|
|
ok = false;
|
|
}
|
|
|
|
lastEntry = entry;
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
private void PreVerifyMft(File file)
|
|
{
|
|
int recordLength = _context.BiosParameterBlock.MftRecordSize;
|
|
int bytesPerSector = _context.BiosParameterBlock.BytesPerSector;
|
|
|
|
// Check out the MFT's clusters
|
|
foreach (var range in file.GetAttribute(AttributeType.Data, null).GetClusters())
|
|
{
|
|
if (!VerifyClusterRange(range))
|
|
{
|
|
ReportError("Corrupt cluster range in MFT data attribute {0}", range.ToString());
|
|
Abort();
|
|
}
|
|
}
|
|
|
|
foreach (var range in file.GetAttribute(AttributeType.Bitmap, null).GetClusters())
|
|
{
|
|
if (!VerifyClusterRange(range))
|
|
{
|
|
ReportError("Corrupt cluster range in MFT bitmap attribute {0}", range.ToString());
|
|
Abort();
|
|
}
|
|
}
|
|
|
|
using (Stream mftStream = file.OpenStream(AttributeType.Data, null, FileAccess.Read))
|
|
using (Stream bitmapStream = file.OpenStream(AttributeType.Bitmap, null, FileAccess.Read))
|
|
{
|
|
Bitmap bitmap = new Bitmap(bitmapStream, long.MaxValue);
|
|
|
|
long index = 0;
|
|
while (mftStream.Position < mftStream.Length)
|
|
{
|
|
byte[] recordData = Utilities.ReadFully(mftStream, recordLength);
|
|
|
|
string magic = Utilities.BytesToString(recordData, 0, 4);
|
|
if (magic != "FILE")
|
|
{
|
|
if (bitmap.IsPresent(index))
|
|
{
|
|
ReportError("Invalid MFT record magic at index {0} - was ({2},{3},{4},{5}) \"{1}\"", index, magic.Trim('\0'), (int)magic[0], (int)magic[1], (int)magic[2], (int)magic[3]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!VerifyMftRecord(recordData, bitmap.IsPresent(index), bytesPerSector))
|
|
{
|
|
ReportError("Invalid MFT record at index {0}", index);
|
|
StringBuilder bldr = new StringBuilder();
|
|
for (int i = 0; i < recordData.Length; ++i)
|
|
{
|
|
bldr.Append(string.Format(CultureInfo.InvariantCulture, " {0:X2}", recordData[i]));
|
|
}
|
|
|
|
ReportInfo("MFT record binary data for index {0}:{1}", index, bldr.ToString());
|
|
}
|
|
}
|
|
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void VerifyMft()
|
|
{
|
|
// Cluster allocation check - check for double allocations
|
|
Dictionary<long, string> clusterMap = new Dictionary<long, string>();
|
|
foreach (FileRecord fr in _context.Mft.Records)
|
|
{
|
|
if ((fr.Flags & FileRecordFlags.InUse) != 0)
|
|
{
|
|
File f = new File(_context, fr);
|
|
foreach (NtfsAttribute attr in f.AllAttributes)
|
|
{
|
|
string attrKey = fr.MasterFileTableIndex + ":" + attr.Id;
|
|
|
|
foreach (var range in attr.GetClusters())
|
|
{
|
|
if (!VerifyClusterRange(range))
|
|
{
|
|
ReportError("Attribute {0} contains bad cluster range {1}", attrKey, range);
|
|
}
|
|
|
|
for (long cluster = range.Offset; cluster < range.Offset + range.Count; ++cluster)
|
|
{
|
|
string existingKey;
|
|
if (clusterMap.TryGetValue(cluster, out existingKey))
|
|
{
|
|
ReportError("Two attributes referencing cluster {0} (0x{0:X16}) - {1} and {2} (as MftIndex:AttrId)", cluster, existingKey, attrKey);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool VerifyMftRecord(byte[] recordData, bool presentInBitmap, int bytesPerSector)
|
|
{
|
|
bool ok = true;
|
|
|
|
//
|
|
// Verify the attributes seem OK...
|
|
//
|
|
byte[] tempBuffer = new byte[recordData.Length];
|
|
Array.Copy(recordData, tempBuffer, tempBuffer.Length);
|
|
GenericFixupRecord genericRecord = new GenericFixupRecord(bytesPerSector);
|
|
genericRecord.FromBytes(tempBuffer, 0);
|
|
|
|
int pos = Utilities.ToUInt16LittleEndian(genericRecord.Content, 0x14);
|
|
while (Utilities.ToUInt32LittleEndian(genericRecord.Content, pos) != 0xFFFFFFFF)
|
|
{
|
|
int attrLen;
|
|
try
|
|
{
|
|
AttributeRecord ar = AttributeRecord.FromBytes(genericRecord.Content, pos, out attrLen);
|
|
if (attrLen != ar.Size)
|
|
{
|
|
ReportError("Attribute size is different to calculated size. AttrId={0}", ar.AttributeId);
|
|
ok = false;
|
|
}
|
|
|
|
if (ar.IsNonResident)
|
|
{
|
|
NonResidentAttributeRecord nrr = (NonResidentAttributeRecord)ar;
|
|
if (nrr.DataRuns.Count > 0)
|
|
{
|
|
long totalVcn = 0;
|
|
foreach (var run in nrr.DataRuns)
|
|
{
|
|
totalVcn += run.RunLength;
|
|
}
|
|
|
|
if (totalVcn != nrr.LastVcn - nrr.StartVcn + 1)
|
|
{
|
|
ReportError("Declared VCNs doesn't match data runs. AttrId={0}", ar.AttributeId);
|
|
ok = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
ReportError("Failure parsing attribute at pos={0}", pos);
|
|
return false;
|
|
}
|
|
|
|
pos += attrLen;
|
|
}
|
|
|
|
//
|
|
// Now consider record as a whole
|
|
//
|
|
FileRecord record = new FileRecord(bytesPerSector);
|
|
record.FromBytes(recordData, 0);
|
|
|
|
bool inUse = (record.Flags & FileRecordFlags.InUse) != 0;
|
|
if (inUse != presentInBitmap)
|
|
{
|
|
ReportError("MFT bitmap and record in-use flag don't agree. Mft={0}, Record={1}", presentInBitmap ? "InUse" : "Free", inUse ? "InUse" : "Free");
|
|
ok = false;
|
|
}
|
|
|
|
if (record.Size != record.RealSize)
|
|
{
|
|
ReportError("MFT record real size is different to calculated size. Stored in MFT={0}, Calculated={1}", record.RealSize, record.Size);
|
|
ok = false;
|
|
}
|
|
|
|
if (Utilities.ToUInt32LittleEndian(recordData, (int)record.RealSize - 8) != uint.MaxValue)
|
|
{
|
|
ReportError("MFT record is not correctly terminated with 0xFFFFFFFF");
|
|
ok = false;
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
private bool VerifyClusterRange(Range<long, long> range)
|
|
{
|
|
bool ok = true;
|
|
if (range.Offset < 0)
|
|
{
|
|
ReportError("Invalid cluster range {0} - negative start", range);
|
|
ok = false;
|
|
}
|
|
|
|
if (range.Count <= 0)
|
|
{
|
|
ReportError("Invalid cluster range {0} - negative/zero count", range);
|
|
ok = false;
|
|
}
|
|
|
|
if ((range.Offset + range.Count) * _context.BiosParameterBlock.BytesPerCluster > _context.RawStream.Length)
|
|
{
|
|
ReportError("Invalid cluster range {0} - beyond end of disk", range);
|
|
ok = false;
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
private void ReportDump(IDiagnosticTraceable toDump)
|
|
{
|
|
_levelsDetected |= ReportLevels.Information;
|
|
if ((_reportLevels & ReportLevels.Information) != 0)
|
|
{
|
|
toDump.Dump(_report, "INFO: ");
|
|
}
|
|
}
|
|
|
|
private void ReportInfo(string str, params object[] args)
|
|
{
|
|
_levelsDetected |= ReportLevels.Information;
|
|
if ((_reportLevels & ReportLevels.Information) != 0)
|
|
{
|
|
_report.WriteLine("INFO: " + str, args);
|
|
}
|
|
}
|
|
|
|
private void ReportError(string str, params object[] args)
|
|
{
|
|
_levelsDetected |= ReportLevels.Errors;
|
|
if ((_reportLevels & ReportLevels.Errors) != 0)
|
|
{
|
|
_report.WriteLine("ERROR: " + str, args);
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
private sealed class AbortException : InvalidFileSystemException
|
|
{
|
|
public AbortException()
|
|
: base()
|
|
{
|
|
}
|
|
|
|
private AbortException(SerializationInfo info, StreamingContext ctxt)
|
|
: base(info, ctxt)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|