mirror of https://github.com/lianthony/NT4.0
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1465 lines
38 KiB
1465 lines
38 KiB
/*++
|
|
|
|
Copyright (c) 1991 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
attrrec.hxx
|
|
|
|
Abstract:
|
|
|
|
This module contains the member function definitions for
|
|
NTFS_ATTRIBUTE_RECORD, which models NTFS attribute records.
|
|
|
|
An Attribute Record may be a template laid over a chunk of
|
|
memory; in that case, it does not own the memory. It may
|
|
also be told, upon initialization, to allocate its own memory
|
|
and copy the supplied data. In that case, it is also responsible
|
|
for freeing that memory.
|
|
|
|
Attribute Records are passed between Attributes and File
|
|
Record Segments. A File Record Segment can initialize
|
|
an Attribute with a list of Attribute Records; when an
|
|
Attribute is Set into a File Record Segment, it packages
|
|
itself up into Attribute Records and inserts them into
|
|
the File Record Segment.
|
|
|
|
File Record Segments also use Attribute Records to scan
|
|
through their list of attribute records, and to shuffle
|
|
them around.
|
|
|
|
Author:
|
|
|
|
Bill McJohn (billmc) 14-June-91
|
|
|
|
Environment:
|
|
|
|
ULIB, User Mode
|
|
|
|
--*/
|
|
|
|
#include <pch.cxx>
|
|
|
|
#define _NTAPI_ULIB_
|
|
#define _UNTFS_MEMBER_
|
|
|
|
#include "ulib.hxx"
|
|
#include "error.hxx"
|
|
#include "untfs.hxx"
|
|
|
|
#include "wstring.hxx"
|
|
#include "extents.hxx"
|
|
#include "attrrec.hxx"
|
|
#include "attrcol.hxx"
|
|
#include "ntfsbit.hxx"
|
|
#include "extents.hxx"
|
|
|
|
#include "upcase.hxx"
|
|
|
|
|
|
DEFINE_EXPORTED_CONSTRUCTOR( NTFS_ATTRIBUTE_RECORD, OBJECT, UNTFS_EXPORT );
|
|
|
|
UNTFS_EXPORT
|
|
NTFS_ATTRIBUTE_RECORD::~NTFS_ATTRIBUTE_RECORD(
|
|
)
|
|
{
|
|
Destroy();
|
|
}
|
|
|
|
VOID
|
|
NTFS_ATTRIBUTE_RECORD::Construct(
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This method is the private worker function for object construction.
|
|
|
|
Arguments:
|
|
|
|
None.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
_Data = NULL;
|
|
_MaximumLength = 0;
|
|
_IsOwnBuffer = FALSE;
|
|
_DisableUnUse = FALSE;
|
|
}
|
|
|
|
VOID
|
|
NTFS_ATTRIBUTE_RECORD::Destroy(
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This method is the private worker function for object destruction.
|
|
|
|
Arguments:
|
|
|
|
None.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
if( _IsOwnBuffer && _Data != NULL ) {
|
|
|
|
FREE( _Data );
|
|
}
|
|
|
|
_Data = NULL;
|
|
_MaximumLength = 0;
|
|
_IsOwnBuffer = FALSE;
|
|
}
|
|
|
|
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::Initialize(
|
|
IN OUT PVOID Data,
|
|
IN ULONG MaximumLength,
|
|
IN BOOLEAN MakeCopy
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This method initializes an NTFS_ATTRIBUTE_RECORD object,
|
|
handing it a buffer with attribute record data. The caller
|
|
may also ask the object to make a private copy of the data.
|
|
|
|
Arguments:
|
|
|
|
Data -- supplies a buffer containing the attribute
|
|
record data the object will own.
|
|
MaximumLength -- supplies the size of the buffer.
|
|
MakeCopy -- supplies a flag indicating whether the object
|
|
should copy the data to a private buffer.
|
|
|
|
Return Value:
|
|
|
|
TRUE upon successful completion.
|
|
|
|
Notes:
|
|
|
|
If MakeCopy is TRUE, then the object must allocate its own
|
|
buffer and copy the attribute record data to it, in which
|
|
case the object is also responsible for freeing that private
|
|
buffer. It that flag is FALSE, then the object will cache a
|
|
pointer to the buffer supplied by the client; the client is
|
|
responsible for making sure that buffer remains valid for
|
|
the lifetime of the NTFS_ATTRIBUTE_RECORD object.
|
|
|
|
This object is reinitializable.
|
|
|
|
--*/
|
|
{
|
|
Destroy();
|
|
|
|
if( !MakeCopy ) {
|
|
|
|
_Data = (PATTRIBUTE_RECORD_HEADER) Data;
|
|
_MaximumLength = MaximumLength;
|
|
_IsOwnBuffer = FALSE;
|
|
|
|
return TRUE;
|
|
|
|
} else {
|
|
|
|
if( (_Data = (PATTRIBUTE_RECORD_HEADER)
|
|
MALLOC( (UINT) MaximumLength )) == NULL ) {
|
|
|
|
Destroy();
|
|
return FALSE;
|
|
}
|
|
|
|
_MaximumLength = MaximumLength;
|
|
_IsOwnBuffer = TRUE;
|
|
memcpy(_Data, Data, (UINT) MaximumLength);
|
|
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
|
|
UNTFS_EXPORT
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::Initialize(
|
|
IN OUT PVOID Data
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This version of Initialize takes it's maximum size from the
|
|
attribute record.
|
|
|
|
Arguments:
|
|
|
|
Data - supplies a buffer containing the attribute
|
|
record data.
|
|
|
|
Return Value:
|
|
|
|
TRUE upon successful completion.
|
|
|
|
--*/
|
|
{
|
|
Destroy();
|
|
|
|
_Data = (PATTRIBUTE_RECORD_HEADER) Data;
|
|
_MaximumLength = _Data->RecordLength;
|
|
_IsOwnBuffer = FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::CreateResidentRecord(
|
|
IN PCVOID Value,
|
|
IN ULONG ValueLength,
|
|
IN ATTRIBUTE_TYPE_CODE TypeCode,
|
|
IN PCWSTRING Name,
|
|
IN USHORT Flags,
|
|
IN UCHAR ResidentFlags
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This method formats the object's buffer with a resident
|
|
attribute record.
|
|
|
|
Arguments:
|
|
|
|
Value -- supplies the attribute value
|
|
ValueLength -- supplies the length of the value
|
|
TypeCode -- supplies the attribute type code
|
|
Name -- supplies the name of the attribute
|
|
(may be NULL)
|
|
Flags -- supplies the attribute's flags.
|
|
ResidentFlags -- supplies the attribute's resident flags
|
|
|
|
Return Value:
|
|
|
|
TRUE upon successful completion.
|
|
|
|
--*/
|
|
{
|
|
// Clear the memory first.
|
|
memset(_Data, 0, (UINT) _MaximumLength);
|
|
|
|
// We will arrange the attribute in the following order:
|
|
// Attribute Record Header
|
|
// Name (if any)
|
|
// Value
|
|
|
|
if( _MaximumLength < SIZE_OF_RESIDENT_HEADER ) {
|
|
|
|
DebugAbort( "Create: buffer is too small.\n" );
|
|
return FALSE;
|
|
}
|
|
|
|
_Data->TypeCode = TypeCode;
|
|
_Data->FormCode = RESIDENT_FORM;
|
|
_Data->Flags = Flags;
|
|
|
|
if( Name != NULL ) {
|
|
|
|
_Data->NameLength = (UCHAR) Name->QueryChCount();
|
|
_Data->NameOffset = DwordAlign(SIZE_OF_RESIDENT_HEADER);
|
|
|
|
_Data->Form.Resident.ValueOffset =
|
|
QuadAlign( _Data->NameOffset +
|
|
_Data->NameLength * sizeof( WCHAR ) );
|
|
|
|
} else {
|
|
|
|
_Data->NameLength = 0;
|
|
_Data->NameOffset = 0;
|
|
|
|
_Data->Form.Resident.ValueOffset =
|
|
QuadAlign(SIZE_OF_RESIDENT_HEADER);
|
|
}
|
|
|
|
_Data->Form.Resident.ValueLength = ValueLength;
|
|
_Data->Form.Resident.ResidentFlags = ResidentFlags;
|
|
|
|
_Data->RecordLength =
|
|
QuadAlign(_Data->Form.Resident.ValueOffset + ValueLength );
|
|
|
|
if( _Data->RecordLength > _MaximumLength ) {
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
// Now that we're sure there's room, copy the name (if any)
|
|
// and the value into their respective places.
|
|
|
|
if( Name != NULL ) {
|
|
|
|
Name->QueryWSTR( 0,
|
|
_Data->NameLength,
|
|
(PWSTR)((PBYTE)_Data + _Data->NameOffset),
|
|
_Data->NameLength,
|
|
FALSE );
|
|
}
|
|
|
|
memcpy( (PBYTE)_Data + _Data->Form.Resident.ValueOffset,
|
|
Value,
|
|
(UINT) ValueLength );
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::CreateNonresidentRecord(
|
|
IN PCNTFS_EXTENT_LIST Extents,
|
|
IN BIG_INT AllocatedLength,
|
|
IN BIG_INT ActualLength,
|
|
IN BIG_INT ValidLength,
|
|
IN ATTRIBUTE_TYPE_CODE TypeCode,
|
|
IN PCWSTRING Name,
|
|
IN USHORT Flags,
|
|
IN USHORT CompressionUnit
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This method formats the attribute record to hold a nonresident
|
|
attribute.
|
|
|
|
Arguments:
|
|
|
|
Extents -- supplies an extent list describing the
|
|
attribute value's disk storage.
|
|
AllocatedLength -- supplies the allocated length of the value
|
|
ActualLength -- supplies the actual length of the value
|
|
ValidLength -- supplies the valid length of the value
|
|
TypeCode -- supplies the attribute type code
|
|
Name -- supplies the name of the attribute
|
|
(may be NULL)
|
|
Flags -- supplies the attribute's flags.
|
|
CompressionUnit -- supplies the log in base 2 of the number of
|
|
clusters per compression unit.
|
|
|
|
--*/
|
|
{
|
|
ULONG MappingPairsLength;
|
|
VCN NextVcn, HighestVcn;
|
|
|
|
// Clear the memory first.
|
|
memset(_Data, 0, (UINT) _MaximumLength);
|
|
|
|
// We will arrange the attribute in the following order:
|
|
// Attribute Record Header
|
|
// Name (if any)
|
|
// Compressed Mapping Pairs
|
|
|
|
if( _MaximumLength < SIZE_OF_NONRESIDENT_HEADER ) {
|
|
|
|
DebugAbort( "Create: buffer is too small.\n" );
|
|
return FALSE;
|
|
}
|
|
|
|
_Data->TypeCode = TypeCode;
|
|
_Data->FormCode = NONRESIDENT_FORM;
|
|
_Data->Flags = Flags;
|
|
|
|
if( Name != NULL ) {
|
|
|
|
_Data->NameLength = (UCHAR) Name->QueryChCount();
|
|
_Data->NameOffset = DwordAlign(SIZE_OF_NONRESIDENT_HEADER);
|
|
|
|
_Data->Form.Nonresident.MappingPairsOffset =
|
|
(USHORT)DwordAlign( _Data->NameOffset +
|
|
_Data->NameLength * sizeof( WCHAR ) );
|
|
|
|
} else {
|
|
|
|
_Data->NameLength = 0;
|
|
_Data->NameOffset = 0;
|
|
|
|
_Data->Form.Nonresident.MappingPairsOffset =
|
|
(USHORT)DwordAlign(SIZE_OF_NONRESIDENT_HEADER);
|
|
}
|
|
|
|
_Data->Form.Nonresident.CompressionUnit = (UCHAR)CompressionUnit;
|
|
|
|
_Data->Form.Nonresident.AllocatedLength =
|
|
AllocatedLength.GetLargeInteger();
|
|
|
|
_Data->Form.Nonresident.FileSize = ActualLength.GetLargeInteger();
|
|
_Data->Form.Nonresident.ValidDataLength = ValidLength.GetLargeInteger();
|
|
|
|
|
|
// Copy the name
|
|
|
|
if( Name != NULL ) {
|
|
|
|
if( (ULONG)(_Data->NameOffset + _Data->NameLength) > _MaximumLength ) {
|
|
|
|
// There isn't enough room for the name.
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
Name->QueryWSTR( 0,
|
|
_Data->NameLength,
|
|
(PWSTR)((PBYTE)_Data + _Data->NameOffset),
|
|
_Data->NameLength,
|
|
FALSE );
|
|
}
|
|
|
|
|
|
if( !Extents->QueryCompressedMappingPairs(
|
|
(PVCN)&(_Data->Form.Nonresident.LowestVcn),
|
|
&NextVcn,
|
|
&MappingPairsLength,
|
|
_MaximumLength -
|
|
_Data->Form.Nonresident.MappingPairsOffset,
|
|
(PVOID)((PBYTE)_Data +
|
|
_Data->Form.Nonresident.MappingPairsOffset) ) ) {
|
|
|
|
// Unable to get the compressed mapping pairs.
|
|
|
|
DebugPrint( "Could not get compressed mapping pairs.\n" );
|
|
return FALSE;
|
|
}
|
|
|
|
HighestVcn = NextVcn - 1;
|
|
memcpy( &_Data->Form.Nonresident.HighestVcn, &HighestVcn, sizeof(VCN) );
|
|
|
|
_Data->RecordLength =
|
|
QuadAlign(_Data->Form.Nonresident.MappingPairsOffset +
|
|
MappingPairsLength );
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::Verify(
|
|
IN PCNTFS_ATTRIBUTE_COLUMNS AttributeDefTable,
|
|
IN BOOLEAN BeLenient
|
|
) CONST
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This routine verifies an attribute record for consistency against
|
|
itself and against the attribute definition table. This routine
|
|
will return FALSE if the attribute record contains any
|
|
inconsistencies.
|
|
|
|
Arguments:
|
|
|
|
AttributeDefTable - Supplies the attribute definition table.
|
|
|
|
Return Value:
|
|
|
|
FALSE - The attribute record is inconsistent.
|
|
TRUE - The attribute record is ok.
|
|
|
|
--*/
|
|
{
|
|
NTFS_EXTENT_LIST extent_list;
|
|
BOOLEAN bad_mapping_pairs;
|
|
ULONG index;
|
|
ULONG column_flags;
|
|
BIG_INT length;
|
|
PFILE_NAME file_name;
|
|
ULONG value_length;
|
|
UCHAR i;
|
|
PWCHAR p;
|
|
|
|
DebugAssert(_Data);
|
|
|
|
// Make sure that we can access at least the form code.
|
|
|
|
if (FIELD_OFFSET(ATTRIBUTE_RECORD_HEADER, Instance) > _Data->RecordLength) {
|
|
DebugPrintf("Attribute form code out-of-bounds.\n");
|
|
return FALSE;
|
|
}
|
|
|
|
// Make sure that the form code is either resident or non-resident.
|
|
|
|
if (_Data->FormCode != RESIDENT_FORM &&
|
|
_Data->FormCode != NONRESIDENT_FORM) {
|
|
|
|
DebugPrintf("Attribute %d has non-existent form code.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
// Make sure that the record is at least as big as the header
|
|
// for the record
|
|
|
|
if (_Data->FormCode == RESIDENT_FORM &&
|
|
_Data->RecordLength < SIZE_OF_RESIDENT_HEADER) {
|
|
|
|
DebugPrintf("Attribute record res header out-of-bounds.\n");
|
|
return FALSE;
|
|
}
|
|
|
|
if (_Data->FormCode == NONRESIDENT_FORM &&
|
|
_Data->RecordLength < SIZE_OF_NONRESIDENT_HEADER) {
|
|
DebugPrintf("Attribute record nonres header out-of-bounds.\n");
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
switch (_Data->TypeCode) {
|
|
|
|
case $STANDARD_INFORMATION:
|
|
|
|
if (!IsResident() ||
|
|
(_Data->Form.Resident.ValueLength !=
|
|
sizeof(STANDARD_INFORMATION) &&
|
|
_Data->Form.Resident.ValueLength !=
|
|
SIZEOF_NEW_STANDARD_INFORMATION)
|
|
) {
|
|
|
|
// This attribute must be resident and at least
|
|
// as big as the above structure.
|
|
|
|
DebugPrintf("The standard information is too small\n");
|
|
return FALSE;
|
|
}
|
|
|
|
// Fall through for next check.
|
|
|
|
case $ATTRIBUTE_LIST:
|
|
case $VOLUME_VERSION:
|
|
case $SECURITY_DESCRIPTOR:
|
|
case $VOLUME_NAME:
|
|
case $VOLUME_INFORMATION:
|
|
case $SYMBOLIC_LINK:
|
|
case $EA_INFORMATION:
|
|
case $EA_DATA:
|
|
|
|
if (_Data->NameLength) {
|
|
|
|
// These attribute may not have names.
|
|
DebugPrintf("Attribute %d should not have a name.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
break;
|
|
|
|
case $INDEX_ALLOCATION:
|
|
|
|
// $INDEX_ALLOCATION's can't be resident.
|
|
|
|
if (IsResident()) {
|
|
DebugPrintf("Attribute %d has resident index allocation\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
// Make sure the name offset if well-aligned and in bounds.
|
|
// Also make sure that the name does not have any unicode NULLs
|
|
// in them.
|
|
|
|
if (_Data->NameLength) {
|
|
|
|
if (_Data->NameOffset%sizeof(WCHAR) ||
|
|
ULONG(_Data->NameOffset + _Data->NameLength) > _Data->RecordLength) {
|
|
|
|
DebugPrintf("Corrupt name for attribute %d.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
p = (PWCHAR) ((PCHAR) _Data + _Data->NameOffset);
|
|
for (i = 0; i < _Data->NameLength; i++) {
|
|
if (!p[i]) {
|
|
DebugPrintf("Unicode NULL in attribute name for attribute %d.\n",
|
|
_Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Make sure that things mesh with the attribute definition table.
|
|
|
|
if (AttributeDefTable) {
|
|
|
|
|
|
if (!AttributeDefTable->QueryIndex(_Data->TypeCode, &index)) {
|
|
|
|
// The attribute type code doesn't exist in the attribute
|
|
// definition table.
|
|
|
|
DebugPrintf("Attribute %d does not exist in the definition table.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
column_flags = AttributeDefTable->QueryFlags(index);
|
|
|
|
if (IsResident() &&
|
|
(_Data->Form.Resident.ResidentFlags & RESIDENT_FORM_INDEXED) &&
|
|
!(column_flags & ATTRIBUTE_DEF_INDEXABLE)) {
|
|
|
|
// Non-indexable indexed attribute.
|
|
|
|
DebugPrintf("Attribute %d is NOT indexable.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
if ((column_flags & ATTRIBUTE_DEF_MUST_BE_INDEXED) &&
|
|
!(IsResident() &&
|
|
(_Data->Form.Resident.ResidentFlags & RESIDENT_FORM_INDEXED))) {
|
|
|
|
// Attribute must be indexed but isn't.
|
|
|
|
DebugPrintf("Attribute %d is MUST be indexed.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
if ((column_flags & ATTRIBUTE_DEF_INDEXABLE) && _Data->NameLength) {
|
|
|
|
// Indexable attributes cannot have names.
|
|
|
|
DebugPrintf("Attribute %d cannot have a name.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
if ((column_flags & ATTRIBUTE_DEF_MUST_BE_NAMED) &&
|
|
!_Data->NameLength) {
|
|
|
|
// Attribute must be named but isn't.
|
|
|
|
DebugPrintf("Attribute %d MUST have a name.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
if ((column_flags & ATTRIBUTE_DEF_MUST_BE_RESIDENT) &&
|
|
!IsResident()) {
|
|
|
|
// Attribute must be resident but isn't.
|
|
|
|
DebugPrintf("Attribute %d MUST be resident.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
if (IsResident()) {
|
|
length = _Data->Form.Resident.ValueLength;
|
|
} else if (_Data->Form.Nonresident.LowestVcn == 0) {
|
|
length = _Data->Form.Nonresident.FileSize;
|
|
} else {
|
|
length = 0;
|
|
}
|
|
|
|
if (length != 0) {
|
|
|
|
if (length < AttributeDefTable->QueryMinimumLength(index)) {
|
|
|
|
// Length is less than the minimum.
|
|
|
|
DebugPrintf("Attribute %d has length less than the minimum.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
// Note that a value of -1 in the Length field of the
|
|
// Attribute Definition Table entry indicates that the
|
|
// attribute can be as large as it pleases.
|
|
//
|
|
// Note the length of the $STANDARD_INFORMATION attribute
|
|
// is checked above.
|
|
//
|
|
|
|
if (AttributeDefTable->QueryMaximumLength(index) != -1 &&
|
|
length > AttributeDefTable->QueryMaximumLength(index) &&
|
|
_Data->TypeCode != $VOLUME_VERSION &&
|
|
_Data->TypeCode != $STANDARD_INFORMATION ) {
|
|
|
|
// Length is greater than the maximum.
|
|
|
|
DebugPrintf("Attribute %d has length greater than the maximum.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (IsResident()) {
|
|
|
|
// Make sure that the value is in bounds and
|
|
// make sure that name comes before value.
|
|
|
|
if (_Data->Form.Resident.ValueLength) {
|
|
|
|
if (_Data->Form.Resident.ValueOffset +
|
|
_Data->Form.Resident.ValueLength >
|
|
_Data->RecordLength) {
|
|
|
|
DebugPrintf("Attribute %d has corrupt resident value.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
if (_Data->NameLength &&
|
|
_Data->NameOffset +
|
|
_Data->NameLength >
|
|
_Data->Form.Resident.ValueOffset) {
|
|
|
|
DebugPrintf("Attribute %d colliding name and resident value.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
// Make sure that if the attribute is indexed then it
|
|
// has no name.
|
|
|
|
if ((_Data->Form.Resident.ResidentFlags & RESIDENT_FORM_INDEXED) &&
|
|
_Data->NameLength) {
|
|
|
|
DebugPrintf("Attribute %d is indexed AND has a name.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
// Make sure that the mapping pairs are in bounds.
|
|
|
|
if (_Data->Form.Nonresident.MappingPairsOffset >=
|
|
_Data->RecordLength) {
|
|
|
|
DebugPrintf("Attribute %d has mapping pairs that are out of bounds.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
if ((QueryFlags() & ATTRIBUTE_FLAG_COMPRESSION_MASK) != 0) {
|
|
|
|
if (_Data->Form.Nonresident.LowestVcn == 0 &&
|
|
_Data->Form.Nonresident.MappingPairsOffset <
|
|
4 * sizeof(BIG_INT)) {
|
|
|
|
DebugPrintf("Attribute %d MappingPairsOffset too small (%d).\n",
|
|
_Data->TypeCode,
|
|
_Data->Form.Nonresident.MappingPairsOffset);
|
|
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
if (_Data->Form.Nonresident.LowestVcn == 0 &&
|
|
_Data->Form.Nonresident.MappingPairsOffset <
|
|
3 * sizeof(BIG_INT)) {
|
|
DebugPrintf("Attribute %d has MappingPairsOffset too small (%d).\n",
|
|
_Data->TypeCode,
|
|
_Data->Form.Nonresident.MappingPairsOffset);
|
|
}
|
|
}
|
|
|
|
// Make sure that the name comes before the mapping pairs.
|
|
|
|
if (_Data->NameLength &&
|
|
_Data->NameOffset +
|
|
_Data->NameLength >
|
|
_Data->Form.Nonresident.MappingPairsOffset) {
|
|
|
|
DebugPrintf("Attribute %d has its name colliding with the mapping pairs.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
// Validate the mapping pairs.
|
|
|
|
if (!extent_list.Initialize(_Data->Form.Nonresident.LowestVcn,
|
|
(PCHAR) _Data +
|
|
_Data->Form.Nonresident.MappingPairsOffset,
|
|
_Data->RecordLength -
|
|
_Data->Form.Nonresident.MappingPairsOffset,
|
|
&bad_mapping_pairs)) {
|
|
|
|
DebugPrintf("Attribute %d has bad mapping pairs.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
if (extent_list.QueryNextVcn() !=
|
|
_Data->Form.Nonresident.HighestVcn + 1 && !BeLenient) {
|
|
|
|
DebugPrintf("Attribute %d has an invalid highest vcn.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
// If the lowest vcn is 0 then make sure that the three sizes
|
|
// make sense.
|
|
|
|
if (_Data->Form.Nonresident.LowestVcn == 0 && !BeLenient) {
|
|
|
|
if (_Data->Form.Nonresident.ValidDataLength >
|
|
_Data->Form.Nonresident.FileSize ||
|
|
_Data->Form.Nonresident.FileSize >
|
|
_Data->Form.Nonresident.AllocatedLength) {
|
|
|
|
DebugPrintf("Attribute %d has inconsistent sizes.\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
|
|
if ((QueryFlags() & ATTRIBUTE_FLAG_COMPRESSION_MASK) != 0 &&
|
|
_Data->Form.Nonresident.TotalAllocated >
|
|
_Data->Form.Nonresident.AllocatedLength) {
|
|
|
|
DebugPrintf("Attribute %d has inconsistent TotalAllocated.\n",
|
|
_Data->TypeCode);
|
|
#if 0
|
|
//
|
|
// This would cause the attribute record to be deleted, which is considered
|
|
// to be too harsh a penalty for this minor error.
|
|
//
|
|
return FALSE;
|
|
#endif
|
|
}
|
|
|
|
if ((QueryFlags() & ATTRIBUTE_FLAG_COMPRESSION_MASK) != 0 &&
|
|
(_Data->Form.Nonresident.AllocatedLength %
|
|
(1 << QueryCompressionUnit())) != 0) {
|
|
|
|
DebugPrintf("Attribute %d has TotalAllocated not multiple of "
|
|
"compression unit\n", _Data->TypeCode);
|
|
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// $FILE_NAME attribute must follow additional special structure.
|
|
|
|
if (_Data->TypeCode == $FILE_NAME) {
|
|
|
|
if (!IsIndexed()) {
|
|
DebugPrintf("File name attribute is not indexed.\n");
|
|
return FALSE;
|
|
}
|
|
|
|
file_name = (PFILE_NAME) ((PCHAR) _Data +
|
|
_Data->Form.Resident.ValueOffset);
|
|
|
|
value_length = _Data->Form.Resident.ValueLength;
|
|
|
|
if (value_length < sizeof(FILE_NAME)) {
|
|
DebugPrintf("Corrupt file name attribute.\n");
|
|
return FALSE;
|
|
}
|
|
|
|
if (NtfsFileNameGetLength(file_name) > value_length) {
|
|
DebugPrintf("Corrupt file name attribute.\n");
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
// Make sure that the file name has no NULL
|
|
// characters in it. If it does then the attribute
|
|
// is "corrupt".
|
|
|
|
for (i = 0; i < file_name->FileNameLength; i++) {
|
|
if (!file_name->FileName[i]) {
|
|
DebugPrintf("Attribute %d has filename w/ null characters\n", _Data->TypeCode);
|
|
return FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::UseClusters(
|
|
IN OUT PNTFS_BITMAP VolumeBitmap,
|
|
OUT PBIG_INT ClusterCount
|
|
) CONST
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This routine allocates the disk space claimed by this attribute
|
|
record in the bitmap provided. A check is made to verify that
|
|
the requested disk space is free before the allocation takes
|
|
place. If the requested space is not available in the bitmap
|
|
then this routine will return FALSE.
|
|
|
|
Arguments:
|
|
|
|
VolumeBitmap - Supplies the bitmap.
|
|
ClusterCount - Receives the number of clusters allocated
|
|
to this record. Not set if method fails.
|
|
|
|
Return Value:
|
|
|
|
FALSE - The request bitmap space was not available.
|
|
TRUE - Success.
|
|
|
|
--*/
|
|
{
|
|
NTFS_EXTENT_LIST extent_list;
|
|
ULONG num_extents;
|
|
ULONG i, j;
|
|
VCN next_vcn;
|
|
LCN current_lcn;
|
|
BIG_INT run_length;
|
|
|
|
DebugAssert(VolumeBitmap);
|
|
|
|
if (IsResident()) {
|
|
*ClusterCount = 0;
|
|
return TRUE;
|
|
}
|
|
|
|
if (!QueryExtentList(&extent_list)) {
|
|
return FALSE;
|
|
}
|
|
|
|
num_extents = extent_list.QueryNumberOfExtents();
|
|
|
|
for (i = 0; i < num_extents; i++) {
|
|
|
|
if (!extent_list.QueryExtent(i, &next_vcn, ¤t_lcn,
|
|
&run_length)) {
|
|
|
|
DebugAbort("Could not query extent");
|
|
return FALSE;
|
|
}
|
|
|
|
if (current_lcn == LCN_NOT_PRESENT) {
|
|
continue;
|
|
}
|
|
|
|
|
|
// Make sure that the run is free before allocating.
|
|
// If it is not, this indicates a cross-link.
|
|
|
|
if (!VolumeBitmap->IsFree(current_lcn, run_length)) {
|
|
|
|
DebugPrintf("cross-linked run starts at 0x%X for 0x%X\n",
|
|
current_lcn.GetLowPart(), run_length.GetLowPart());
|
|
|
|
// Free everything so far allocated by this routine.
|
|
|
|
for (j = 0; j < i; j++) {
|
|
|
|
if (!extent_list.QueryExtent(j, &next_vcn, ¤t_lcn,
|
|
&run_length)) {
|
|
|
|
DebugAbort("Could not query extent");
|
|
return FALSE;
|
|
}
|
|
if (current_lcn == LCN_NOT_PRESENT) {
|
|
continue;
|
|
}
|
|
|
|
VolumeBitmap->SetFree(current_lcn, run_length);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
VolumeBitmap->SetAllocated(current_lcn, run_length);
|
|
}
|
|
|
|
*ClusterCount = extent_list.QueryClustersAllocated();
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::UseClusters(
|
|
IN OUT PNTFS_BITMAP VolumeBitmap,
|
|
OUT PBIG_INT ClusterCount,
|
|
IN ULONG AllowCrossLinkStart,
|
|
IN ULONG AllowCrossLinkLength,
|
|
OUT PBOOLEAN DidCrossLinkOccur
|
|
) CONST
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This routine allocates the disk space claimed by this attribute
|
|
record in the bitmap provided. A check is made to verify that
|
|
the requested disk space is free before the allocation takes
|
|
place. If the requested space is not available in the bitmap
|
|
then this routine will return FALSE.
|
|
|
|
This methode assumes that the range specified by the Allow
|
|
parameters are marked as allocated in the given bitmap.
|
|
|
|
Arguments:
|
|
|
|
VolumeBitmap - Supplies the bitmap.
|
|
ClusterCount - Receives the number of clusters allocated
|
|
to this record. Not set if method fails.
|
|
AllowCrossLinkStart - Supplies the start of a range where
|
|
cross-links are allowed.
|
|
AllowCrossLinkLength - Supplies the length of the range where
|
|
cross-links are allowed.
|
|
DidCrossLinkOccur - Returns whether or not an allowable
|
|
cross-link occurred.
|
|
|
|
Return Value:
|
|
|
|
FALSE - The request bitmap space was not available.
|
|
TRUE - Success.
|
|
|
|
--*/
|
|
{
|
|
BOOLEAN r;
|
|
|
|
DebugAssert(DidCrossLinkOccur);
|
|
|
|
*DidCrossLinkOccur = FALSE;
|
|
|
|
if (UseClusters(VolumeBitmap,ClusterCount)) {
|
|
return TRUE;
|
|
}
|
|
|
|
*DidCrossLinkOccur = TRUE;
|
|
|
|
VolumeBitmap->SetFree(AllowCrossLinkStart, AllowCrossLinkLength);
|
|
|
|
r = UseClusters(VolumeBitmap,ClusterCount);
|
|
|
|
VolumeBitmap->SetAllocated(AllowCrossLinkStart, AllowCrossLinkLength);
|
|
|
|
return r;
|
|
}
|
|
|
|
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::UnUseClusters(
|
|
IN OUT PNTFS_BITMAP VolumeBitmap,
|
|
IN ULONG LeaveInUseStart,
|
|
IN ULONG LeaveInUseLength
|
|
) CONST
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This operation reverses a successful 'UseClusters' operation.
|
|
|
|
This method assumes that the LeaveInUse range is already in
|
|
use by the bitmap.
|
|
|
|
Arguments:
|
|
|
|
VolumeBitmap - Supplies the bitmap.
|
|
LeaveInUseStart - Supplies the start of the range that this routine
|
|
should leave in use.
|
|
LeaveInUseLength - Supplies the length of the range that this
|
|
routine should leave in use.
|
|
|
|
Return Value:
|
|
|
|
FALSE - Failure.
|
|
TRUE - Success.
|
|
|
|
--*/
|
|
{
|
|
NTFS_EXTENT_LIST extent_list;
|
|
ULONG num_extents;
|
|
ULONG i;
|
|
VCN next_vcn;
|
|
LCN current_lcn;
|
|
BIG_INT run_length;
|
|
|
|
DebugAssert(VolumeBitmap);
|
|
|
|
if (IsResident() || _DisableUnUse) {
|
|
return TRUE;
|
|
}
|
|
|
|
if (!QueryExtentList(&extent_list)) {
|
|
return FALSE;
|
|
}
|
|
|
|
num_extents = extent_list.QueryNumberOfExtents();
|
|
|
|
for (i = 0; i < num_extents; i++) {
|
|
|
|
if (!extent_list.QueryExtent(i, &next_vcn, ¤t_lcn,
|
|
&run_length)) {
|
|
|
|
DebugAbort("Could not query extent");
|
|
return FALSE;
|
|
}
|
|
if (LCN_NOT_PRESENT == current_lcn) {
|
|
continue;
|
|
}
|
|
|
|
VolumeBitmap->SetFree(current_lcn, run_length);
|
|
}
|
|
|
|
VolumeBitmap->SetAllocated(LeaveInUseStart, LeaveInUseLength);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
NONVIRTUAL
|
|
UNTFS_EXPORT
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::QueryName(
|
|
OUT PWSTRING Name
|
|
) CONST
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This method returns the name of the attribute.
|
|
|
|
Arguments:
|
|
|
|
Name - Returns the name of the attribute.
|
|
|
|
Return Value:
|
|
|
|
FALSE - Failure.
|
|
TRUE - Success.
|
|
|
|
--*/
|
|
{
|
|
if (FIELD_OFFSET(ATTRIBUTE_RECORD_HEADER, Flags) >= _MaximumLength ||
|
|
ULONG(_Data->NameOffset + _Data->NameLength) > _MaximumLength ||
|
|
_Data->NameLength == 0) {
|
|
|
|
return Name->Initialize( "" );
|
|
|
|
} else {
|
|
|
|
return Name->Initialize((PWSTR)((PBYTE)_Data + _Data->NameOffset),
|
|
_Data->NameLength);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
VOID
|
|
NTFS_ATTRIBUTE_RECORD::QueryValueLength(
|
|
OUT PBIG_INT ValueLength,
|
|
OUT PBIG_INT AllocatedLength,
|
|
OUT PBIG_INT ValidLength,
|
|
OUT PBIG_INT TotalAllocated
|
|
) CONST
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This method returns the actual, allocated, valid, and
|
|
total allocated lengths
|
|
of the attribute value associated with this record.
|
|
|
|
If the attribute is resident, these values are all
|
|
the length of the resident value, except total allocated,
|
|
which is meaningless.
|
|
|
|
If the attribute is nonresident, these four values are only
|
|
meaningful if the LowestVcn of this attribute record is 0.
|
|
Additionally, TotalAllocated is only valid for compressed
|
|
attributes.
|
|
|
|
Arguments:
|
|
|
|
ValueLength -- receives the actual length of the value.
|
|
AllocatedLength -- receives the allocated size of the value.
|
|
(may be NULL if the caller doesn't care)
|
|
ValidLength -- receives the valid length of the value.
|
|
(may be NULL if the caller doesn't care)
|
|
TotalAllocated -- receives the total allocated length of the
|
|
value (may be NULL).
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
DebugPtrAssert( _Data );
|
|
|
|
if( _Data->FormCode == RESIDENT_FORM ) {
|
|
|
|
*ValueLength = _Data->Form.Resident.ValueLength;
|
|
|
|
if( AllocatedLength != NULL ) {
|
|
|
|
*AllocatedLength = _Data->Form.Resident.ValueLength;
|
|
}
|
|
|
|
if( ValidLength != NULL ) {
|
|
|
|
*ValidLength = _Data->Form.Resident.ValueLength;
|
|
}
|
|
|
|
if (TotalAllocated != NULL ) {
|
|
|
|
// no such value for resident attributes
|
|
|
|
*TotalAllocated = 0;
|
|
}
|
|
|
|
} else {
|
|
|
|
DebugAssert( _Data->FormCode == NONRESIDENT_FORM );
|
|
|
|
*ValueLength = _Data->Form.Nonresident.FileSize;
|
|
|
|
if( AllocatedLength != NULL ) {
|
|
|
|
*AllocatedLength = _Data->Form.Nonresident.AllocatedLength;
|
|
}
|
|
|
|
if( ValidLength != NULL ) {
|
|
|
|
*ValidLength = _Data->Form.Nonresident.ValidDataLength;
|
|
}
|
|
|
|
if (TotalAllocated != NULL) {
|
|
if ((_Data->Flags & ATTRIBUTE_FLAG_COMPRESSION_MASK) != 0) {
|
|
*TotalAllocated = _Data->Form.Nonresident.TotalAllocated;
|
|
} else {
|
|
*TotalAllocated = 0;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
VOID
|
|
NTFS_ATTRIBUTE_RECORD::SetTotalAllocated(
|
|
IN BIG_INT TotalAllocated
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Set the "TotalAllocated" field in the attribute record. If the
|
|
attribute record doesn't have a total allocated field because
|
|
the attribute isn't compressed or because it's resident, this
|
|
method has no effect.
|
|
|
|
Arguments:
|
|
|
|
TotalAllocated - the new value.
|
|
|
|
Return Value:
|
|
|
|
None.
|
|
|
|
--*/
|
|
{
|
|
DebugPtrAssert( _Data );
|
|
|
|
if( _Data->FormCode == RESIDENT_FORM ) {
|
|
|
|
// no such value for resident attributes; ignore
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
DebugAssert( _Data->FormCode == NONRESIDENT_FORM );
|
|
|
|
if ((_Data->Flags & ATTRIBUTE_FLAG_COMPRESSION_MASK) != 0) {
|
|
_Data->Form.Nonresident.TotalAllocated =
|
|
TotalAllocated.GetLargeInteger();
|
|
}
|
|
}
|
|
|
|
|
|
UNTFS_EXPORT
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::QueryExtentList(
|
|
OUT PNTFS_EXTENT_LIST ExtentList
|
|
) CONST
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Arguments:
|
|
|
|
None.
|
|
|
|
Return Value:
|
|
|
|
A pointer to the extent list. A return value of NULL indicates
|
|
that the attribute is resident or that an error occurred processing
|
|
the compressed mapping pairs. (Clients should use IsResident to
|
|
determine whether the attribute value is resident.)
|
|
|
|
--*/
|
|
{
|
|
DebugPtrAssert( _Data );
|
|
|
|
if( _Data->FormCode == NONRESIDENT_FORM &&
|
|
ExtentList->Initialize( _Data->Form.Nonresident.LowestVcn,
|
|
(PVOID)((PBYTE)_Data +
|
|
_Data->Form.Nonresident.MappingPairsOffset),
|
|
_MaximumLength -
|
|
_Data->Form.Nonresident.
|
|
MappingPairsOffset ) ) {
|
|
|
|
return TRUE;
|
|
|
|
} else {
|
|
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
|
|
BOOLEAN
|
|
NTFS_ATTRIBUTE_RECORD::IsMatch(
|
|
IN ATTRIBUTE_TYPE_CODE Type,
|
|
IN PCWSTRING Name,
|
|
IN PCVOID Value,
|
|
IN ULONG ValueLength
|
|
) CONST
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This method determines whether the attribute record matches the
|
|
parameters given.
|
|
|
|
Arguments:
|
|
|
|
Type -- Supplies the type code of the attribute. This
|
|
is the primary key, and must always be present.
|
|
Name -- Supplies a name to match. A name of NULL is the
|
|
same as specifying the null string.
|
|
Value -- Supplies the value to match. If this argument is
|
|
null, any value matches. Only resident
|
|
attributes can be checked for value matches.
|
|
ValueLength -- Supplies the length of the value (if any).
|
|
|
|
Notes:
|
|
|
|
Value matching is not supported for nonresident attribute values;
|
|
if a Value parameter is supplied, then no non-resident attribute
|
|
records will match.
|
|
|
|
--*/
|
|
{
|
|
DSTRING RecordName;
|
|
|
|
DebugPtrAssert( _Data );
|
|
|
|
if( Type != _Data->TypeCode ) {
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
if( Name != NULL ) {
|
|
|
|
if( !RecordName.Initialize((PWSTR)((PBYTE)_Data + _Data->NameOffset),
|
|
_Data->NameLength ) ) {
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
if( Name->Strcmp( &RecordName ) != 0 ) {
|
|
|
|
return FALSE;
|
|
}
|
|
} else if (_Data->NameLength) {
|
|
return FALSE;
|
|
}
|
|
|
|
if( Value != NULL &&
|
|
( _Data->FormCode != RESIDENT_FORM ||
|
|
ValueLength != _Data->Form.Resident.ValueLength ||
|
|
memcmp( Value,
|
|
(PBYTE)_Data + _Data->Form.Resident.ValueOffset,
|
|
(UINT) ValueLength ) ) ) {
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
LONG
|
|
CompareAttributeRecords(
|
|
IN PCNTFS_ATTRIBUTE_RECORD Left,
|
|
IN PCNTFS_ATTRIBUTE_RECORD Right,
|
|
IN PCNTFS_UPCASE_TABLE UpcaseTable
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This method compares two attribute records to determine their
|
|
correct ordering in the File Record Segment.
|
|
|
|
Arguments:
|
|
|
|
Left -- Supplies the left-hand operand of the comparison.
|
|
Right -- Supplies the right-hand operand of the comparison.
|
|
UpcaseTable -- Supplies the upcase table for the volume.
|
|
If this parameter is NULL, name comparison
|
|
cannot be performed.
|
|
|
|
Return Value:
|
|
|
|
<0 if Left is less than Right
|
|
0 if Left equals Right
|
|
>0 if Left is greater than Right.
|
|
|
|
Notes:
|
|
|
|
Attribute records are ordered first by type code and then
|
|
by name. An attribute record without a name is less than
|
|
any attribute record of the same type with a name.
|
|
|
|
Name comparision is first done case-insensitive; if the names
|
|
are equal by that metric, a case-sensitive comparision is made.
|
|
|
|
The UpcaseTable parameter may be omitted if either or both names
|
|
are zero-length, or if they are identical (including case).
|
|
Otherwise, it must be supplied.
|
|
|
|
--*/
|
|
{
|
|
ULONG Result;
|
|
|
|
// First, compare the type codes:
|
|
//
|
|
Result = Left->QueryTypeCode() - Right->QueryTypeCode();
|
|
|
|
if( Result != 0 ) {
|
|
|
|
return Result;
|
|
}
|
|
|
|
// They have the same type code, so we have to compare the names.
|
|
// Pass in TRUE for the IsAttribute parameter, to indicate that
|
|
// we are comparing attribute names.
|
|
//
|
|
return( NtfsUpcaseCompare( Left->GetName(),
|
|
Left->QueryNameLength(),
|
|
Right->GetName(),
|
|
Right->QueryNameLength(),
|
|
UpcaseTable,
|
|
TRUE ) );
|
|
}
|