X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/72a12576750f52947eb043106ba5c12c0d07decf..b1ab9ed8d0e0f1c3b66d7daa8fd5564444c56195:/libsecurity_filedb/lib/AppleDatabase.cpp?ds=inline diff --git a/libsecurity_filedb/lib/AppleDatabase.cpp b/libsecurity_filedb/lib/AppleDatabase.cpp new file mode 100644 index 00000000..dbe9ecec --- /dev/null +++ b/libsecurity_filedb/lib/AppleDatabase.cpp @@ -0,0 +1,2561 @@ +/* + * Copyright (c) 2000-2001, 2003 Apple Computer, Inc. All Rights Reserved. + * + * The contents of this file constitute Original Code as defined in and are + * subject to the Apple Public Source License Version 1.2 (the 'License'). + * You may not use this file except in compliance with the License. Please obtain + * a copy of the License at http://www.apple.com/publicsource and read it before + * using this file. + * + * This Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS + * OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT + * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the + * specific language governing rights and limitations under the License. + */ + + +// +// AppleDatabase.cpp - Description t.b.d. +// +#include "AppleDatabase.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *kAppleDatabaseChanged = "com.apple.AppleDatabaseChanged"; + +/* Number of seconds after which we open/pread/close a db to check it's + version number even if we didn't get any notifications. Note that we always + check just after we take a write lock and whenever we get a notification + that any db on the system has changed. */ +static const CFTimeInterval kForceReReadTime = 15.0; + +/* Token on which we receive notifications and the pthread_once_t protecting + it's initialization. */ +pthread_once_t gCommonInitMutex = PTHREAD_ONCE_INIT; + +/* Global counter of how many notifications we have received and a lock to + protect the counter. */ +static int kSegmentSize = 4; +int32_t* gSegment = NULL; + +/* Registration routine for notifcations. Called inside a pthread_once(). */ +static void initCommon(void) +{ + // open the file + int segmentDescriptor = shm_open (kAppleDatabaseChanged, O_RDWR | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO); + if (segmentDescriptor < 0) + { + return; + } + + // set the segment size + ftruncate (segmentDescriptor, kSegmentSize); + + // map it into memory + int32_t* tmp = (int32_t*) mmap (NULL, kSegmentSize, PROT_READ | PROT_WRITE, MAP_SHARED, segmentDescriptor, 0); + close (segmentDescriptor); + + if (tmp == (int32_t*) -1) // can't map the memory? + { + gSegment = NULL; + } + else + { + gSegment = tmp; + } +} + +// +// Table +// +Table::Table(const ReadSection &inTableSection) : + mMetaRecord(inTableSection[OffsetId]), + mTableSection(inTableSection), + mRecordsCount(inTableSection[OffsetRecordsCount]), + mFreeListHead(inTableSection[OffsetFreeListHead]), + mRecordNumbersCount(inTableSection[OffsetRecordNumbersCount]) +{ + // can't easily initialize indexes here, since meta record is incomplete + // until much later... see DbVersion::open() +} + +Table::~Table() +{ + for_each_map_delete(mIndexMap.begin(), mIndexMap.end()); +} + +void +Table::readIndexSection() +{ + uint32 indexSectionOffset = mTableSection.at(OffsetIndexesOffset); + + uint32 numIndexes = mTableSection.at(indexSectionOffset + AtomSize); + + for (uint32 i = 0; i < numIndexes; i++) { + uint32 indexOffset = mTableSection.at(indexSectionOffset + (i + 2) * AtomSize); + ReadSection indexSection(mTableSection.subsection(indexOffset)); + + auto_ptr index(new DbConstIndex(*this, indexSection)); + mIndexMap.insert(ConstIndexMap::value_type(index->indexId(), index.get())); + index.release(); + } +} + +Cursor * +Table::createCursor(const CSSM_QUERY *inQuery, const DbVersion &inDbVersion) const +{ + // if an index matches the query, return a cursor which uses the index + + ConstIndexMap::const_iterator it; + DbQueryKey *queryKey; + + for (it = mIndexMap.begin(); it != mIndexMap.end(); it++) + if (it->second->matchesQuery(*inQuery, queryKey)) { + IndexCursor *cursor = new IndexCursor(queryKey, inDbVersion, *this, it->second); + return cursor; + } + + // otherwise, return a cursor that iterates over all table records + + return new LinearCursor(inQuery, inDbVersion, *this); +} + +const ReadSection +Table::getRecordSection(uint32 inRecordNumber) const +{ + if (inRecordNumber >= mRecordNumbersCount) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORD_UID); + + uint32 aRecordOffset = mTableSection[OffsetRecordNumbers + AtomSize + * inRecordNumber]; + + // Check if this RecordNumber has been deleted. + if (aRecordOffset & 1 || aRecordOffset == 0) + CssmError::throwMe(CSSMERR_DL_RECORD_NOT_FOUND); + + return MetaRecord::readSection(mTableSection, aRecordOffset); +} + +const RecordId +Table::getRecord(const RecordId &inRecordId, + CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR inoutAttributes, + CssmData *inoutData, + Allocator &inAllocator) const +{ + const ReadSection aRecordSection = getRecordSection(inRecordId.mRecordNumber); + const RecordId aRecordId = MetaRecord::unpackRecordId(aRecordSection); + + // Make sure the RecordNumber matches that in the RecordId we just retrived. + if (aRecordId.mRecordNumber != inRecordId.mRecordNumber) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + + if (aRecordId.mCreateVersion != inRecordId.mCreateVersion) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORD_UID); + + // XXX Figure out which value to pass for inQueryFlags (5th) argument + mMetaRecord.unpackRecord(aRecordSection, inAllocator, inoutAttributes, + inoutData, 0); + return aRecordId; +} + +uint32 +Table::popFreeList(uint32 &aFreeListHead) const +{ + assert(aFreeListHead | 1); + uint32 anOffset = aFreeListHead ^ 1; + uint32 aRecordNumber = (anOffset - OffsetRecordNumbers) / AtomSize; + aFreeListHead = mTableSection[anOffset]; + return aRecordNumber; +} + +const ReadSection +Table::getRecordsSection() const +{ + return mTableSection.subsection(mTableSection[OffsetRecords]); +} + +bool +Table::matchesTableId(Id inTableId) const +{ + Id anId = mMetaRecord.dataRecordType(); + if (inTableId == CSSM_DL_DB_RECORD_ANY) // All non schema tables. + return !(CSSM_DB_RECORDTYPE_SCHEMA_START <= anId + && anId < CSSM_DB_RECORDTYPE_SCHEMA_END); + + if (inTableId == CSSM_DL_DB_RECORD_ALL_KEYS) // All key tables. + return (anId == CSSM_DL_DB_RECORD_PUBLIC_KEY + || anId == CSSM_DL_DB_RECORD_PRIVATE_KEY + || anId == CSSM_DL_DB_RECORD_SYMMETRIC_KEY); + + return inTableId == anId; // Only if exact match. +} + + +// +// ModifiedTable +// +ModifiedTable::ModifiedTable(const Table *inTable) : + mTable(inTable), + mNewMetaRecord(nil), + mRecordNumberCount(inTable->recordNumberCount()), + mFreeListHead(inTable->freeListHead()), + mIsModified(false) +{ +} + +ModifiedTable::ModifiedTable(MetaRecord *inMetaRecord) : + mTable(nil), + mNewMetaRecord(inMetaRecord), + mRecordNumberCount(0), + mFreeListHead(0), + mIsModified(true) +{ +} + +ModifiedTable::~ModifiedTable() +{ + for_each_map_delete(mIndexMap.begin(), mIndexMap.end()); + for_each_map_delete(mInsertedMap.begin(), mInsertedMap.end()); + + delete mNewMetaRecord; +} + +void +ModifiedTable::deleteRecord(const RecordId &inRecordId) +{ + modifyTable(); + + uint32 aRecordNumber = inRecordId.mRecordNumber; + + // remove the record from all the indexes + MutableIndexMap::iterator it; + for (it = mIndexMap.begin(); it != mIndexMap.end(); it++) + it->second->removeRecord(aRecordNumber); + + InsertedMap::iterator anIt = mInsertedMap.find(aRecordNumber); + if (anIt == mInsertedMap.end()) + { + // If we have no old table than this record can not exist yet. + if (!mTable) + CssmError::throwMe(CSSMERR_DL_RECORD_NOT_FOUND); + +#if RECORDVERSIONCHECK + const RecordId aRecordId = MetaRecord::unpackRecordId(mTable->getRecordSection(aRecordNumber)); + if (aRecordId.mRecordVersion != inRecordId.mRecordVersion) + CssmError::throwMe(CSSMERR_DL_RECORD_MODIFIED); +#endif + + // Schedule the record for deletion + if (!mDeletedSet.insert(aRecordNumber).second) + CssmError::throwMe(CSSMERR_DL_RECORD_NOT_FOUND); // It was already deleted + } + else + { + const RecordId aRecordId = MetaRecord::unpackRecordId(*anIt->second); + if (aRecordId.mCreateVersion != inRecordId.mCreateVersion) + CssmError::throwMe(CSSMERR_DL_RECORD_NOT_FOUND); + +#if RECORDVERSIONCHECK + if (aRecordId.mRecordVersion != inRecordId.mRecordVersion) + CssmError::throwMe(CSSMERR_DL_RECORD_MODIFIED); +#endif + + // Remove the inserted (but uncommited) record. It should already be in mDeletedSet + // if it existed previously in mTable. + delete anIt->second; + mInsertedMap.erase(anIt); + } +} + +const RecordId +ModifiedTable::insertRecord(uint32 inVersionId, + const CSSM_DB_RECORD_ATTRIBUTE_DATA *inAttributes, + const CssmData *inData) +{ + modifyTable(); + + auto_ptr aWriteSection(new WriteSection()); + getMetaRecord().packRecord(*aWriteSection, inAttributes, inData); + uint32 aRecordNumber = nextRecordNumber(); + + // add the record to all the indexes; this will throw if the new record + // violates a unique index + MutableIndexMap::iterator it; + for (it = mIndexMap.begin(); it != mIndexMap.end(); it++) + it->second->insertRecord(aRecordNumber, *(aWriteSection.get())); + + // schedule the record for insertion + RecordId aRecordId(aRecordNumber, inVersionId); + MetaRecord::packRecordId(aRecordId, *aWriteSection); + mInsertedMap.insert(InsertedMap::value_type(aRecordNumber, aWriteSection.get())); + + aWriteSection.release(); + + return aRecordId; +} + +const RecordId +ModifiedTable::updateRecord(const RecordId &inRecordId, + const CSSM_DB_RECORD_ATTRIBUTE_DATA *inAttributes, + const CssmData *inData, + CSSM_DB_MODIFY_MODE inModifyMode) +{ + modifyTable(); + + uint32 aRecordNumber = inRecordId.mRecordNumber; + InsertedMap::iterator anIt = mInsertedMap.find(aRecordNumber); + + // aReUpdate is true iff we are updating an already updated record. + bool aReUpdate = anIt != mInsertedMap.end(); + + // If we are not re-updating and there is no old table than this record does not exist yet. + if (!aReUpdate && !mTable) + CssmError::throwMe(CSSMERR_DL_RECORD_NOT_FOUND); + + const ReadSection &anOldDbRecord = aReUpdate ? *anIt->second : mTable->getRecordSection(aRecordNumber); + const RecordId aRecordId = MetaRecord::unpackRecordId(anOldDbRecord); + + // Did someone else delete the record we are trying to update. + if (aRecordId.mCreateVersion != inRecordId.mCreateVersion) + CssmError::throwMe(CSSMERR_DL_RECORD_NOT_FOUND); + +#if RECORDVERSIONCHECK + // Is the record we that our update is based on current? + if (aRecordId.mRecordVersion != inRecordId.mRecordVersion) + CssmError::throwMe(CSSMERR_DL_STALE_UNIQUE_RECORD); +#endif + + // Update the actual packed record. + auto_ptr aDbRecord(new WriteSection()); + getMetaRecord().updateRecord(anOldDbRecord, *aDbRecord, + CssmDbRecordAttributeData::overlay(inAttributes), inData, inModifyMode); + + + // Bump the RecordVersion of this record. + RecordId aNewRecordId(aRecordNumber, inRecordId.mCreateVersion, inRecordId.mRecordVersion + 1); + // Store the RecordVersion in the packed aDbRecord. + MetaRecord::packRecordId(aNewRecordId, *aDbRecord); + + if (!aReUpdate && !mDeletedSet.insert(aRecordNumber).second) + CssmError::throwMe(CSSMERR_DL_RECORD_NOT_FOUND); // Record was already in mDeletedSet + + // remove the original record from all the indexes + MutableIndexMap::iterator it; + for (it = mIndexMap.begin(); it != mIndexMap.end(); it++) + it->second->removeRecord(aRecordNumber); + + try + { + // Add the updated record to all the indexes; this will throw if the new record + // violates a unique index + for (it = mIndexMap.begin(); it != mIndexMap.end(); it++) + it->second->insertRecord(aRecordNumber, *(aDbRecord.get())); + + if (aReUpdate) + { + // Get rid of anOldDbRecord from the inserted map and replace it + // with aDbRecord. + delete anIt->second; + anIt->second = aDbRecord.get(); + } + else + { + // First time though so let's just put the new value in the map. + mInsertedMap.insert(InsertedMap::value_type(aRecordNumber, aDbRecord.get())); + } + aDbRecord.release(); + } + catch(...) + { + // We only remove aRecordNumber from mDeletedSet if we added it above. + if (!aReUpdate) + mDeletedSet.erase(aRecordNumber); + + // The 2 operations below are an attempt to preserve the indices when + // an insert fails. + + // Remove the updated record from all the indexes + MutableIndexMap::iterator it; + for (it = mIndexMap.begin(); it != mIndexMap.end(); it++) + it->second->removeRecord(aRecordNumber); + + // Add the original record back to all the indexes + for (it = mIndexMap.begin(); it != mIndexMap.end(); it++) + it->second->insertRecord(aRecordNumber, anOldDbRecord); + + throw; + } + + return aNewRecordId; +} + +const RecordId +ModifiedTable::getRecord(const RecordId &inRecordId, + CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR inoutAttributes, + CssmData *inoutData, + Allocator &inAllocator) const +{ + if (mIsModified) + { + uint32 aRecordNumber = inRecordId.mRecordNumber; + InsertedMap::const_iterator anIt = mInsertedMap.find(aRecordNumber); + if (anIt != mInsertedMap.end()) + { + // We found the record in mInsertedMap so we use the inserted + // record. + const ReadSection &aRecordSection = *(anIt->second); + const RecordId aRecordId = MetaRecord::unpackRecordId(aRecordSection); + + // Make sure the RecordNumber matches that in the RecordId we just retrived. + if (aRecordId.mRecordNumber != aRecordNumber) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + + if (aRecordId.mCreateVersion != inRecordId.mCreateVersion) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORD_UID); + + // XXX Figure out which value to pass for inQueryFlags (5th) argument + getMetaRecord().unpackRecord(aRecordSection, inAllocator, + inoutAttributes, inoutData, 0); + + return aRecordId; + } + else if (mDeletedSet.find(aRecordNumber) != mDeletedSet.end()) + { + // If aRecordNumber was not in mInsertedMap but it was in + // mDeletedSet then it was deleted but not yet commited. + CssmError::throwMe(CSSMERR_DL_RECORD_NOT_FOUND); + } + } + + if (!mTable) + CssmError::throwMe(CSSMERR_DL_RECORD_NOT_FOUND); + + // Either this table wasn't modified yet or we didn't find aRecordNumber in + // mInsertedMap nor mDeletedSet so just ask mTable for it. + return mTable->getRecord(inRecordId, inoutAttributes, inoutData, + inAllocator); +} + +uint32 +ModifiedTable::nextRecordNumber() +{ + // If we still have unused free records in mTable get the next one. + if (mFreeListHead) + return mTable->popFreeList(mFreeListHead); + + // Bump up the mRecordNumberCount so we don't reuse the same one. + return mRecordNumberCount++; +} + +uint32 +ModifiedTable::recordNumberCount() const +{ + uint32 anOldMax = !mTable ? 0 : mTable->recordNumberCount() - 1; + uint32 anInsertedMax = mInsertedMap.empty() ? 0 : mInsertedMap.rbegin()->first; + + DeletedSet::reverse_iterator anIt = mDeletedSet.rbegin(); + DeletedSet::reverse_iterator anEnd = mDeletedSet.rend(); + for (; anIt != anEnd; anIt++) + { + if (*anIt != anOldMax || anOldMax <= anInsertedMax) + break; + anOldMax--; + } + + return max(anOldMax,anInsertedMax) + 1; +} + +const MetaRecord & +ModifiedTable::getMetaRecord() const +{ + return mNewMetaRecord ? *mNewMetaRecord : mTable->getMetaRecord(); +} + +// prepare to modify the table + +void +ModifiedTable::modifyTable() +{ + if (!mIsModified) { + createMutableIndexes(); + mIsModified = true; + } +} + +// create mutable indexes from the read-only indexes in the underlying table + +void +ModifiedTable::createMutableIndexes() +{ + if (mTable == NULL) + return; + + Table::ConstIndexMap::const_iterator it; + for (it = mTable->mIndexMap.begin(); it != mTable->mIndexMap.end(); it++) { + auto_ptr mutableIndex(new DbMutableIndex(*it->second)); + mIndexMap.insert(MutableIndexMap::value_type(it->first, mutableIndex.get())); + mutableIndex.release(); + } +} + +// find, and create if needed, an index with the given id + +DbMutableIndex & +ModifiedTable::findIndex(uint32 indexId, const MetaRecord &metaRecord, bool isUniqueIndex) +{ + MutableIndexMap::iterator it = mIndexMap.find(indexId); + + if (it == mIndexMap.end()) { + // create the new index + auto_ptr index(new DbMutableIndex(metaRecord, indexId, isUniqueIndex)); + it = mIndexMap.insert(MutableIndexMap::value_type(indexId, index.get())).first; + index.release(); + } + + return *it->second; +} + +uint32 +ModifiedTable::writeIndexSection(WriteSection &tableSection, uint32 offset) +{ + MutableIndexMap::iterator it; + + tableSection.put(Table::OffsetIndexesOffset, offset); + + // leave room for the size, to be written later + uint32 indexSectionOffset = offset; + offset += AtomSize; + + offset = tableSection.put(offset, mIndexMap.size()); + + // leave room for the array of offsets to the indexes + uint32 indexOffsetOffset = offset; + offset += mIndexMap.size() * AtomSize; + + // write the indexes + for (it = mIndexMap.begin(); it != mIndexMap.end(); it++) { + indexOffsetOffset = tableSection.put(indexOffsetOffset, offset); + offset = it->second->writeIndex(tableSection, offset); + } + + // write the total index section size + tableSection.put(indexSectionOffset, offset - indexSectionOffset); + + return offset; +} + +uint32 +ModifiedTable::writeTable(AtomicTempFile &inAtomicTempFile, uint32 inSectionOffset) +{ + if (mTable && !mIsModified) { + // the table has not been modified, so we can just dump the old table + // section into the new database + + const ReadSection &tableSection = mTable->getTableSection(); + uint32 tableSize = tableSection.at(Table::OffsetSize); + + inAtomicTempFile.write(AtomicFile::FromStart, inSectionOffset, + tableSection.range(Range(0, tableSize)), tableSize); + + return inSectionOffset + tableSize; + } + + // We should have an old mTable or a mNewMetaRecord but not both. + assert(mTable != nil ^ mNewMetaRecord != nil); + const MetaRecord &aNewMetaRecord = getMetaRecord(); + + uint32 aRecordsCount = 0; + uint32 aRecordNumbersCount = recordNumberCount(); + uint32 aRecordsOffset = Table::OffsetRecordNumbers + AtomSize * aRecordNumbersCount; + WriteSection aTableSection(Allocator::standard(), aRecordsOffset); + aTableSection.size(aRecordsOffset); + aTableSection.put(Table::OffsetId, aNewMetaRecord.dataRecordType()); + aTableSection.put(Table::OffsetRecords, aRecordsOffset); + aTableSection.put(Table::OffsetRecordNumbersCount, aRecordNumbersCount); + + uint32 anOffset = inSectionOffset + aRecordsOffset; + + if (mTable) + { + // XXX Handle schema changes in the future. + assert(mNewMetaRecord == nil); + + // We have a modified old table so copy all non deleted records + // The code below is rather elaborate, but this is because it attempts + // to copy large ranges of non deleted records with single calls + // to AtomicFile::write() + uint32 anOldRecordsCount = mTable->getRecordsCount(); + ReadSection aRecordsSection = mTable->getRecordsSection(); + uint32 aReadOffset = 0; // Offset of current record + uint32 aWriteOffset = aRecordsOffset; // Offset for current write record + uint32 aBlockStart = aReadOffset; // Starting point for read + uint32 aBlockSize = 0; // Size of block to read + for (uint32 aRecord = 0; aRecord < anOldRecordsCount; aRecord++) + { + ReadSection aRecordSection = MetaRecord::readSection(aRecordsSection, aReadOffset); + uint32 aRecordNumber = MetaRecord::unpackRecordNumber(aRecordSection); + uint32 aRecordSize = aRecordSection.size(); + aReadOffset += aRecordSize; + if (mDeletedSet.find(aRecordNumber) == mDeletedSet.end()) + { + // This record has not been deleted. Register the offset + // at which it will be in the new file in aTableSection. + aTableSection.put(Table::OffsetRecordNumbers + + AtomSize * aRecordNumber, + aWriteOffset); + aWriteOffset += aRecordSize; + aBlockSize += aRecordSize; + aRecordsCount++; + // XXX update all indexes being created. + } + else + { + // The current record has been deleted. Copy all records up + // to but not including the current one to the new file. + if (aBlockSize > 0) + { + inAtomicTempFile.write(AtomicFile::FromStart, anOffset, + aRecordsSection.range(Range(aBlockStart, + aBlockSize)), + aBlockSize); + anOffset += aBlockSize; + } + + // Set the start of the next block to the start of the next + // record, and the size of the block to 0. + aBlockStart = aReadOffset; + aBlockSize = 0; + } // if (mDeletedSet..) + } // for (aRecord...) + + // Copy all records that have not yet been copied to the new file. + if (aBlockSize > 0) + { + inAtomicTempFile.write(AtomicFile::FromStart, anOffset, + aRecordsSection.range(Range(aBlockStart, + aBlockSize)), + aBlockSize); + anOffset += aBlockSize; + } + } // if (mTable) + + // Now add all inserted records to the table. + InsertedMap::const_iterator anIt = mInsertedMap.begin(); + InsertedMap::const_iterator anEnd = mInsertedMap.end(); + // Iterate over all inserted objects. + for (; anIt != anEnd; anIt++) + { + // Write out each inserted/modified record + const WriteSection &aRecord = *anIt->second; + uint32 aRecordNumber = anIt->first; + // Put offset relative to start of this table in recordNumber array. + aTableSection.put(Table::OffsetRecordNumbers + AtomSize * aRecordNumber, + anOffset - inSectionOffset); + inAtomicTempFile.write(AtomicFile::FromStart, anOffset, + aRecord.address(), aRecord.size()); + anOffset += aRecord.size(); + aRecordsCount++; + // XXX update all indexes being created. + } + + // Reconstruct the freelist (this is O(N) where N is the number of recordNumbers) + // We could implement it faster by using the old freelist and skipping the records + // that have been inserted. However building the freelist for the newly used + // recordNumbers (not in mTable) would look like the code below anyway (starting + // from mTable->recordNumberCount()). + // The first part of this would be O(M Log(N)) (where M is the old number of + // free records, and N is the number of newly inserted records) + // The second part would be O(N) where N is the currently max RecordNumber + // in use - the old max RecordNumber in use. + uint32 aFreeListHead = 0; // Link to previous free record + for (uint32 aRecordNumber = 0; aRecordNumber < aRecordNumbersCount; aRecordNumber++) + { + // Make the freelist a list of all records with 0 offset (non existing). + if (!aTableSection.at(Table::OffsetRecordNumbers + AtomSize * aRecordNumber)) + { + aTableSection.put(Table::OffsetRecordNumbers + + AtomSize * aRecordNumber, + aFreeListHead); + // Make aFreeListHead point to the previous free recordNumber slot in the table. + aFreeListHead = (Table::OffsetRecordNumbers + AtomSize * aRecordNumber) | 1; + } + } + aTableSection.put(Table::OffsetFreeListHead, aFreeListHead); + + anOffset -= inSectionOffset; + + // Write out indexes, which are part of the table section + + { + uint32 indexOffset = anOffset; + anOffset = writeIndexSection(aTableSection, anOffset); + inAtomicTempFile.write(AtomicFile::FromStart, inSectionOffset + indexOffset, + aTableSection.address() + indexOffset, anOffset - indexOffset); + } + + // Set the section size and recordCount. + aTableSection.put(Table::OffsetSize, anOffset); + aTableSection.put(Table::OffsetRecordsCount, aRecordsCount); + + // Write out aTableSection header. + inAtomicTempFile.write(AtomicFile::FromStart, inSectionOffset, + aTableSection.address(), aTableSection.size()); + + return anOffset + inSectionOffset; +} + + + +// +// Metadata +// + +// Attribute definitions + +static const CSSM_DB_ATTRIBUTE_INFO RelationID = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "RelationID"}, + CSSM_DB_ATTRIBUTE_FORMAT_UINT32 +}; +static const CSSM_DB_ATTRIBUTE_INFO RelationName = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "RelationName"}, + CSSM_DB_ATTRIBUTE_FORMAT_STRING +}; +static const CSSM_DB_ATTRIBUTE_INFO AttributeID = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "AttributeID"}, + CSSM_DB_ATTRIBUTE_FORMAT_UINT32 +}; +static const CSSM_DB_ATTRIBUTE_INFO AttributeNameFormat = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "AttributeNameFormat"}, + CSSM_DB_ATTRIBUTE_FORMAT_UINT32 +}; +static const CSSM_DB_ATTRIBUTE_INFO AttributeName = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "AttributeName"}, + CSSM_DB_ATTRIBUTE_FORMAT_STRING +}; +static const CSSM_DB_ATTRIBUTE_INFO AttributeNameID = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "AttributeNameID"}, + CSSM_DB_ATTRIBUTE_FORMAT_BLOB +}; +static const CSSM_DB_ATTRIBUTE_INFO AttributeFormat = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "AttributeFormat"}, + CSSM_DB_ATTRIBUTE_FORMAT_UINT32 +}; +static const CSSM_DB_ATTRIBUTE_INFO IndexID = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "IndexID"}, + CSSM_DB_ATTRIBUTE_FORMAT_UINT32 +}; +static const CSSM_DB_ATTRIBUTE_INFO IndexType = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "IndexType"}, + CSSM_DB_ATTRIBUTE_FORMAT_UINT32 +}; +static const CSSM_DB_ATTRIBUTE_INFO IndexedDataLocation = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "IndexedDataLocation"}, + CSSM_DB_ATTRIBUTE_FORMAT_UINT32 +}; +static const CSSM_DB_ATTRIBUTE_INFO ModuleID = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "ModuleID"}, + CSSM_DB_ATTRIBUTE_FORMAT_BLOB +}; +static const CSSM_DB_ATTRIBUTE_INFO AddinVersion = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "AddinVersion"}, + CSSM_DB_ATTRIBUTE_FORMAT_STRING +}; +static const CSSM_DB_ATTRIBUTE_INFO SSID = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "SSID"}, + CSSM_DB_ATTRIBUTE_FORMAT_UINT32 +}; +static const CSSM_DB_ATTRIBUTE_INFO SubserviceType = +{ + CSSM_DB_ATTRIBUTE_NAME_AS_STRING, + {(char*) "SubserviceType"}, + CSSM_DB_ATTRIBUTE_FORMAT_UINT32 +}; + +#define ATTRIBUTE(type, name) \ + { CSSM_DB_ATTRIBUTE_NAME_AS_STRING, { (char*) #name }, CSSM_DB_ATTRIBUTE_FORMAT_ ## type } + +static const CSSM_DB_ATTRIBUTE_INFO AttrSchemaRelations[] = +{ + //RelationID, RelationName + ATTRIBUTE(UINT32, RelationID), + ATTRIBUTE(STRING, RelationName) +}; + +static const CSSM_DB_ATTRIBUTE_INFO AttrSchemaAttributes[] = +{ + //RelationID, AttributeID, + //AttributeNameFormat, AttributeName, AttributeNameID, + //AttributeFormat + ATTRIBUTE(UINT32, RelationID), + ATTRIBUTE(UINT32, AttributeID), + ATTRIBUTE(UINT32, AttributeNameFormat), + ATTRIBUTE(STRING, AttributeName), + ATTRIBUTE(BLOB, AttributeNameID), + ATTRIBUTE(UINT32, AttributeFormat) +}; + +static const CSSM_DB_ATTRIBUTE_INFO AttrSchemaIndexes[] = +{ + ATTRIBUTE(UINT32, RelationID), + ATTRIBUTE(UINT32, IndexID), + ATTRIBUTE(UINT32, AttributeID), + ATTRIBUTE(UINT32, IndexType), + ATTRIBUTE(UINT32, IndexedDataLocation) + //RelationID, IndexID, AttributeID, + //IndexType, IndexedDataLocation +}; + +static const CSSM_DB_ATTRIBUTE_INFO AttrSchemaParsingModule[] = +{ + ATTRIBUTE(UINT32, RelationID), + ATTRIBUTE(UINT32, AttributeID), + ATTRIBUTE(BLOB, ModuleID), + ATTRIBUTE(STRING, AddinVersion), + ATTRIBUTE(UINT32, SSID), + ATTRIBUTE(UINT32, SubserviceType) + //RelationID, AttributeID, + //ModuleID, AddinVersion, SSID, SubserviceType +}; + +#undef ATTRIBUTE + +// +// DbVersion +// +DbVersion::DbVersion(const AppleDatabase &db, const RefPointer &inAtomicBufferedFile) : + mDatabase(reinterpret_cast(NULL), 0), + mDb(db), + mBufferedFile(inAtomicBufferedFile) +{ + off_t aLength = mBufferedFile->length(); + off_t bytesRead = 0; + const uint8 *ptr = mBufferedFile->read(0, aLength, bytesRead); + mBufferedFile->close(); + mDatabase = ReadSection(ptr, bytesRead); + open(); +} + +DbVersion::~DbVersion() +{ + try + { + for_each_map_delete(mTableMap.begin(), mTableMap.end()); + } + catch(...) {} +} + +void +DbVersion::open() +{ + try + { + // This is the oposite of DbModifier::commit() + mVersionId = mDatabase[mDatabase.size() - AtomSize]; + + const ReadSection aHeaderSection = mDatabase.subsection(HeaderOffset, + HeaderSize); + if (aHeaderSection.at(OffsetMagic) != HeaderMagic) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + + // We currently only support one version. If we support additional + // file format versions in the future fix this. + uint32 aVersion = aHeaderSection.at(OffsetVersion); + if (aVersion != HeaderVersion) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + + //const ReadSection anAuthSection = + // mDatabase.subsection(HeaderOffset + aHeaderSection.at(OffsetAuthOffset)); + // XXX Do something with anAuthSection. + + uint32 aSchemaOffset = aHeaderSection.at(OffsetSchemaOffset); + const ReadSection aSchemaSection = + mDatabase.subsection(HeaderOffset + aSchemaOffset); + + uint32 aSchemaSize = aSchemaSection[OffsetSchemaSize]; + // Make sure that the given range exists. + aSchemaSection.subsection(0, aSchemaSize); + uint32 aTableCount = aSchemaSection[OffsetTablesCount]; + + // Assert that the size of this section is big enough. + if (aSchemaSize < OffsetTables + AtomSize * aTableCount) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + + for (uint32 aTableNumber = 0; aTableNumber < aTableCount; + aTableNumber++) + { + uint32 aTableOffset = aSchemaSection.at(OffsetTables + AtomSize + * aTableNumber); + // XXX Set the size boundary on aTableSection. + const ReadSection aTableSection = + aSchemaSection.subsection(aTableOffset); + auto_ptr aTable(new Table(aTableSection)); + Table::Id aTableId = aTable->getMetaRecord().dataRecordType(); + mTableMap.insert(TableMap::value_type(aTableId, aTable.get())); + aTable.release(); + } + + // Fill in the schema for the meta tables. + + findTable(mDb.schemaRelations.DataRecordType).getMetaRecord(). + setRecordAttributeInfo(mDb.schemaRelations); + findTable(mDb.schemaIndexes.DataRecordType).getMetaRecord(). + setRecordAttributeInfo(mDb.schemaIndexes); + findTable(mDb.schemaParsingModule.DataRecordType).getMetaRecord(). + setRecordAttributeInfo(mDb.schemaParsingModule); + + // OK, we have created all the tables in the tableMap. Now + // lets read the schema and proccess it accordingly. + // Iterate over all schema records. + Table &aTable = findTable(mDb.schemaAttributes.DataRecordType); + aTable.getMetaRecord().setRecordAttributeInfo(mDb.schemaAttributes); + uint32 aRecordsCount = aTable.getRecordsCount(); + ReadSection aRecordsSection = aTable.getRecordsSection(); + uint32 aReadOffset = 0; + const MetaRecord &aMetaRecord = aTable.getMetaRecord(); + + CSSM_DB_ATTRIBUTE_DATA aRelationIDData = + { + RelationID, + 0, + NULL + }; + CSSM_DB_ATTRIBUTE_DATA aAttributeIDData = + { + AttributeID, + 0, + NULL + }; + CSSM_DB_ATTRIBUTE_DATA aAttributeNameFormatData = + { + AttributeNameFormat, + 0, + NULL + }; + CSSM_DB_ATTRIBUTE_DATA aAttributeNameData = + { + AttributeName, + 0, + NULL + }; + CSSM_DB_ATTRIBUTE_DATA aAttributeNameIDData = + { + AttributeNameID, + 0, + NULL + }; + CSSM_DB_ATTRIBUTE_DATA aAttributeFormatData = + { + AttributeFormat, + 0, + NULL + }; + CSSM_DB_ATTRIBUTE_DATA aRecordAttributes[] = + { + aRelationIDData, + aAttributeIDData, + aAttributeNameFormatData, + aAttributeNameData, + aAttributeNameIDData, + aAttributeFormatData + }; + CSSM_DB_RECORD_ATTRIBUTE_DATA aRecordAttributeData = + { + aMetaRecord.dataRecordType(), + 0, + sizeof(aRecordAttributes) / sizeof(CSSM_DB_ATTRIBUTE_DATA), + aRecordAttributes + }; + CssmDbRecordAttributeData &aRecordData = CssmDbRecordAttributeData::overlay(aRecordAttributeData); + + TrackingAllocator recordAllocator(Allocator::standard()); + for (uint32 aRecord = 0; aRecord != aRecordsCount; aRecord++) + { + ReadSection aRecordSection = MetaRecord::readSection(aRecordsSection, aReadOffset); + uint32 aRecordSize = aRecordSection.size(); + aReadOffset += aRecordSize; + aMetaRecord.unpackRecord(aRecordSection, recordAllocator, + &aRecordAttributeData, NULL, 0); + // Create the attribute coresponding to this entry + if (aRecordData[0].size() != 1 || aRecordData[0].format() != CSSM_DB_ATTRIBUTE_FORMAT_UINT32) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + uint32 aRelationId = aRecordData[0]; + + // Skip the schema relations for the meta tables themselves. + // FIXME: this hard-wires the meta-table relation IDs to be + // within {CSSM_DB_RECORDTYPE_SCHEMA_START... + // CSSM_DB_RECORDTYPE_SCHEMA_END} (which is {0..4}). + // Bogus - the MDS schema relation IDs start at + // CSSM_DB_RELATIONID_MDS_START which is 0x40000000. + // Ref. Radar 2817921. + if (CSSM_DB_RECORDTYPE_SCHEMA_START <= aRelationId && aRelationId < CSSM_DB_RECORDTYPE_SCHEMA_END) + continue; + + // Get the MetaRecord corresponding to the specified RelationId + MetaRecord &aMetaRecord = findTable(aRelationId).getMetaRecord(); + + if (aRecordData[1].size() != 1 + || aRecordData[1].format() != CSSM_DB_ATTRIBUTE_FORMAT_UINT32 + || aRecordData[2].size() != 1 + || aRecordData[2].format() != CSSM_DB_ATTRIBUTE_FORMAT_UINT32 + || aRecordData[5].size() != 1 + || aRecordData[5].format() != CSSM_DB_ATTRIBUTE_FORMAT_UINT32) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + + uint32 anAttributeId = aRecordData[1]; + uint32 anAttributeNameFormat = aRecordData[2]; + uint32 anAttributeFormat = aRecordData[5]; + auto_ptr aName; + const CssmData *aNameID = NULL; + + if (aRecordData[3].size() == 1) + { + if (aRecordData[3].format() != CSSM_DB_ATTRIBUTE_FORMAT_STRING) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + + auto_ptr aName2(new string(static_cast(aRecordData[3]))); + aName = aName2; + } + + if (aRecordData[4].size() == 1) + { + if (aRecordData[4].format() != CSSM_DB_ATTRIBUTE_FORMAT_BLOB) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + + // @@@ Invoking conversion operator to CssmData & on aRecordData[4] + // And taking address of result. + aNameID = &static_cast(aRecordData[4]); + } + + // Make sure that the attribute specified by anAttributeNameFormat is present. + switch (anAttributeNameFormat) + { + case CSSM_DB_ATTRIBUTE_NAME_AS_STRING: + if (aRecordData[3].size() != 1) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + break; + case CSSM_DB_ATTRIBUTE_NAME_AS_OID: + if (aRecordData[4].size() != 1) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + break; + case CSSM_DB_ATTRIBUTE_NAME_AS_INTEGER: + break; + default: + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + } + + // Create the attribute + aMetaRecord.createAttribute(aName.get(), aNameID, anAttributeId, anAttributeFormat); + } + + // initialize the indexes associated with each table + { + TableMap::iterator it; + for (it = mTableMap.begin(); it != mTableMap.end(); it++) + it->second->readIndexSection(); + } + } + catch(...) + { + for_each_map_delete(mTableMap.begin(), mTableMap.end()); + mTableMap.clear(); + throw; + } +} + +const RecordId +DbVersion::getRecord(Table::Id inTableId, const RecordId &inRecordId, + CSSM_DB_RECORD_ATTRIBUTE_DATA *inoutAttributes, + CssmData *inoutData, + Allocator &inAllocator) const +{ + return findTable(inTableId).getRecord(inRecordId, inoutAttributes, + inoutData, inAllocator); +} + +Cursor * +DbVersion::createCursor(const CSSM_QUERY *inQuery) const +{ + // XXX We should add support for these special query types + // By Creating a Cursor that iterates over multiple tables + if (!inQuery || inQuery->RecordType == CSSM_DL_DB_RECORD_ANY + || inQuery->RecordType == CSSM_DL_DB_RECORD_ALL_KEYS) + { + return new MultiCursor(inQuery, *this); + } + + return findTable(inQuery->RecordType).createCursor(inQuery, *this); +} + +bool DbVersion::hasTable(Table::Id inTableId) const +{ + TableMap::const_iterator it = mTableMap.find(inTableId); + return it != mTableMap.end(); +} + +const Table & +DbVersion::findTable(Table::Id inTableId) const +{ + TableMap::const_iterator it = mTableMap.find(inTableId); + if (it == mTableMap.end()) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORDTYPE); + return *it->second; +} + +Table & +DbVersion::findTable(Table::Id inTableId) +{ + TableMap::iterator it = mTableMap.find(inTableId); + if (it == mTableMap.end()) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + return *it->second; +} + +// +// Cursor implemetation +// +Cursor::Cursor() +{ +} + +Cursor::Cursor(const DbVersion &inDbVersion) : mDbVersion(&inDbVersion) +{ +} + +Cursor::~Cursor() +{ +} + +bool +Cursor::next(Table::Id &outTableId, + CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR outAttributes, + CssmData *outData, + Allocator &inAllocator, + RecordId &recordId) +{ + return false; +} + +// +// LinearCursor implemetation +// +LinearCursor::LinearCursor(const CSSM_QUERY *inQuery, const DbVersion &inDbVersion, + const Table &inTable) : + Cursor(inDbVersion), + mRecordsCount(inTable.getRecordsCount()), + mRecord(0), + mRecordsSection(inTable.getRecordsSection()), + mReadOffset(0), + mMetaRecord(inTable.getMetaRecord()) +{ + if (inQuery) + { + mConjunctive = inQuery->Conjunctive; + mQueryFlags = inQuery->QueryFlags; + // XXX Do something with inQuery->QueryLimits? + uint32 aPredicatesCount = inQuery->NumSelectionPredicates; + mPredicates.resize(aPredicatesCount); + try + { + for (uint32 anIndex = 0; anIndex < aPredicatesCount; anIndex++) + { + CSSM_SELECTION_PREDICATE &aPredicate = inQuery->SelectionPredicate[anIndex]; + mPredicates[anIndex] = new SelectionPredicate(mMetaRecord, aPredicate); + } + } + catch(...) + { + for_each_delete(mPredicates.begin(), mPredicates.end()); + throw; + } + } +} + +LinearCursor::~LinearCursor() +{ + for_each_delete(mPredicates.begin(), mPredicates.end()); +} + +bool +LinearCursor::next(Table::Id &outTableId, + CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR inoutAttributes, + CssmData *inoutData, Allocator &inAllocator, RecordId &recordId) +{ + while (mRecord++ < mRecordsCount) + { + ReadSection aRecordSection = MetaRecord::readSection(mRecordsSection, mReadOffset); + uint32 aRecordSize = aRecordSection.size(); + mReadOffset += aRecordSize; + + PredicateVector::const_iterator anIt = mPredicates.begin(); + PredicateVector::const_iterator anEnd = mPredicates.end(); + bool aMatch; + if (anIt == anEnd) + { + // If there are no predicates we have a match. + aMatch = true; + } + else if (mConjunctive == CSSM_DB_OR) + { + // If mConjunctive is OR, the first predicate that returns + // true indicates a match. Dropthough means no match + aMatch = false; + for (; anIt != anEnd; anIt++) + { + if ((*anIt)->evaluate(aRecordSection)) + { + aMatch = true; + break; + } + } + } + else if (mConjunctive == CSSM_DB_AND || mConjunctive == CSSM_DB_NONE) + { + // If mConjunctive is AND (or NONE), the first predicate that returns + // false indicates a mismatch. Dropthough means a match + aMatch = true; + for (; anIt != anEnd; anIt++) + { + if (!(*anIt)->evaluate(aRecordSection)) + { + aMatch = false; + break; + } + } + } + else + { + // XXX Should be CSSMERR_DL_INVALID_QUERY (or CSSMERR_DL_INVALID_CONJUNTIVE). + CssmError::throwMe(CSSMERR_DL_UNSUPPORTED_QUERY); + } + + if (aMatch) + { + // Get the actual record. + mMetaRecord.unpackRecord(aRecordSection, inAllocator, + inoutAttributes, inoutData, + mQueryFlags); + outTableId = mMetaRecord.dataRecordType(); + recordId = MetaRecord::unpackRecordId(aRecordSection); + return true; + } + } + + return false; +} + +// +// IndexCursor +// + +IndexCursor::IndexCursor(DbQueryKey *queryKey, const DbVersion &inDbVersion, + const Table &table, const DbConstIndex *index) : + Cursor(inDbVersion), mQueryKey(queryKey), mTable(table), mIndex(index) +{ + index->performQuery(*queryKey, mBegin, mEnd); +} + +IndexCursor::~IndexCursor() +{ + // the query key will be deleted automatically, since it's an auto_ptr +} + +bool +IndexCursor::next(Table::Id &outTableId, + CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR outAttributes, + CssmData *outData, + Allocator &inAllocator, RecordId &recordId) +{ + if (mBegin == mEnd) + return false; + + ReadSection rs = mIndex->getRecordSection(mBegin++); + const MetaRecord &metaRecord = mTable.getMetaRecord(); + + outTableId = metaRecord.dataRecordType(); + metaRecord.unpackRecord(rs, inAllocator, outAttributes, outData, 0); + + recordId = MetaRecord::unpackRecordId(rs); + return true; +} + +// +// MultiCursor +// +MultiCursor::MultiCursor(const CSSM_QUERY *inQuery, const DbVersion &inDbVersion) : + Cursor(inDbVersion), mTableIterator(inDbVersion.begin()) +{ + if (inQuery) + mQuery.reset(new CssmAutoQuery(*inQuery)); + else + { + mQuery.reset(new CssmAutoQuery()); + mQuery->recordType(CSSM_DL_DB_RECORD_ANY); + } +} + +MultiCursor::~MultiCursor() +{ +} + +bool +MultiCursor::next(Table::Id &outTableId, + CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR inoutAttributes, + CssmData *inoutData, Allocator &inAllocator, RecordId &recordId) +{ + for (;;) + { + if (!mCursor.get()) + { + if (mTableIterator == mDbVersion->end()) + return false; + + const Table &aTable = *mTableIterator++; + if (!aTable.matchesTableId(mQuery->recordType())) + continue; + + mCursor.reset(aTable.createCursor(mQuery.get(), *mDbVersion)); + } + + if (mCursor->next(outTableId, inoutAttributes, inoutData, inAllocator, recordId)) + return true; + + mCursor.reset(NULL); + } +} + + +// +// DbModifier +// +DbModifier::DbModifier(AtomicFile &inAtomicFile, const AppleDatabase &db) : + Metadata(), + mDbVersion(), + mAtomicFile(inAtomicFile), + mDb(db) +{ +} + +DbModifier::~DbModifier() +{ + try + { + for_each_map_delete(mModifiedTableMap.begin(), mModifiedTableMap.end()); + // mAtomicTempFile will do automatic rollback on destruction. + } + catch(...) {} +} + +const RefPointer +DbModifier::getDbVersion(bool force) +{ + StLock _(mDbVersionLock); + + /* Initialize the shared memory file change mechanism */ + pthread_once(&gCommonInitMutex, initCommon); + + /* If we don't have a mDbVersion yet, or we are force to re-read the file + before a write transaction, or we have received any notifications after + the last time we read the file, or more than kForceReReadTime seconds + have passed since the last time we read the file, we open the file and + check if it has changed. */ + if (!mDbVersion || + force || + gSegment == NULL || + mNotifyCount != *gSegment || + CFAbsoluteTimeGetCurrent() > mDbLastRead + kForceReReadTime) + { + RefPointer atomicBufferedFile(mAtomicFile.read()); + off_t length = atomicBufferedFile->open(); + /* Record the number of notifications we've seen and when we last + opened the file. */ + if (gSegment != NULL) + { + mNotifyCount = *gSegment; + } + + mDbLastRead = CFAbsoluteTimeGetCurrent(); + + /* If we already have a mDbVersion, let's check if we can reuse it. */ + if (mDbVersion) + { + if (length < AtomSize) + CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + + off_t bytesRead = 0; + const uint8 *ptr = atomicBufferedFile->read(length - AtomSize, + AtomSize, bytesRead); + ReadSection aVersionSection(ptr, bytesRead); + uint32 aVersionId = aVersionSection[0]; + + /* If the version stamp hasn't changed the old mDbVersion is still + current. */ + if (aVersionId == mDbVersion->getVersionId()) + return mDbVersion; + } + + mDbVersion = new DbVersion(mDb, atomicBufferedFile); + } + + return mDbVersion; +} + +void +DbModifier::createDatabase(const CSSM_DBINFO &inDbInfo, + const CSSM_ACL_ENTRY_INPUT *inInitialAclEntry, + mode_t mode) +{ + // XXX This needs better locking. There is a possible race condition between + // two concurrent creators. Or a writer/creator or a close/create etc. + if (mAtomicTempFile || !mModifiedTableMap.empty()) + CssmError::throwMe(CSSMERR_DL_DATASTORE_ALREADY_EXISTS); + + mAtomicTempFile = mAtomicFile.create(mode); + // Set mVersionId to one since this is the first version of the database. + mVersionId = 1; + + // we need to create the meta tables first, because inserting tables + // (including the meta tables themselves) relies on them being there + createTable(new MetaRecord(mDb.schemaRelations)); + createTable(new MetaRecord(mDb.schemaAttributes)); + createTable(new MetaRecord(mDb.schemaIndexes)); + createTable(new MetaRecord(mDb.schemaParsingModule)); + + // now add the meta-tables' schema to the meta tables themselves + insertTableSchema(mDb.schemaRelations); + insertTableSchema(mDb.schemaAttributes); + insertTableSchema(mDb.schemaIndexes); + insertTableSchema(mDb.schemaParsingModule); + + if (inInitialAclEntry != NULL) + { + //createACL(*inInitialAclEntry); + } + + if (inDbInfo.NumberOfRecordTypes == 0) + return; + if (inDbInfo.RecordAttributeNames == NULL) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORDTYPE); + if (inDbInfo.RecordIndexes == NULL) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORD_INDEX); + if (inDbInfo.DefaultParsingModules == NULL) + CssmError::throwMe(CSSMERR_DL_INVALID_PARSING_MODULE); + + for (uint32 anIndex = 0; anIndex < inDbInfo.NumberOfRecordTypes; anIndex++) + { + insertTable(CssmDbRecordAttributeInfo::overlay(inDbInfo.RecordAttributeNames[anIndex]), + &inDbInfo.RecordIndexes[anIndex], + &inDbInfo.DefaultParsingModules[anIndex]); + } +} + +void DbModifier::openDatabase() +{ + // No need to do anything on open if we are already writing the database. + if (!mAtomicTempFile) + getDbVersion(false); +} + +void DbModifier::closeDatabase() +{ + commit(); // XXX Requires write lock. + StLock _(mDbVersionLock); + mDbVersion = NULL; +} + +void DbModifier::deleteDatabase() +{ + bool isDirty = mAtomicTempFile; + rollback(); // XXX Requires write lock. + StLock _(mDbVersionLock); + + // Clean up mModifiedTableMap in case this object gets reused again for + // a new create. + for_each_map_delete(mModifiedTableMap.begin(), mModifiedTableMap.end()); + mModifiedTableMap.clear(); + + // If the database was dirty and we had no mDbVersion yet then rollback() + // would have deleted the db. + if (!isDirty || mDbVersion) + { + mDbVersion = NULL; + mAtomicFile.performDelete(); + } +} + +void +DbModifier::modifyDatabase() +{ + if (mAtomicTempFile) + return; + + try + { + mAtomicTempFile = mAtomicFile.write(); + // Now we are holding the write lock make sure we get the latest greatest version of the db. + // Also set mVersionId to one more that that of the old database. + mVersionId = getDbVersion(true)->getVersionId() + 1; + + // Never make a database with mVersionId 0 since it makes bad things happen to Jaguar and older systems + if (mVersionId == 0) + mVersionId = 1; + + // Remove all old modified tables + for_each_map_delete(mModifiedTableMap.begin(), mModifiedTableMap.end()); + mModifiedTableMap.clear(); + + // Setup the new tables + DbVersion::TableMap::const_iterator anIt = + mDbVersion->mTableMap.begin(); + DbVersion::TableMap::const_iterator anEnd = + mDbVersion->mTableMap.end(); + for (; anIt != anEnd; ++anIt) + { + auto_ptr aTable(new ModifiedTable(anIt->second)); + mModifiedTableMap.insert(ModifiedTableMap::value_type(anIt->first, + aTable.get())); + aTable.release(); + } + } + catch(...) + { + for_each_map_delete(mModifiedTableMap.begin(), mModifiedTableMap.end()); + mModifiedTableMap.clear(); + rollback(); + throw; + } +} + +void +DbModifier::deleteRecord(Table::Id inTableId, const RecordId &inRecordId) +{ + modifyDatabase(); + findTable(inTableId).deleteRecord(inRecordId); +} + +const RecordId +DbModifier::insertRecord(Table::Id inTableId, + const CSSM_DB_RECORD_ATTRIBUTE_DATA *inAttributes, + const CssmData *inData) +{ + modifyDatabase(); + return findTable(inTableId).insertRecord(mVersionId, inAttributes, inData); +} + +const RecordId +DbModifier::updateRecord(Table::Id inTableId, const RecordId &inRecordId, + const CSSM_DB_RECORD_ATTRIBUTE_DATA *inAttributes, + const CssmData *inData, + CSSM_DB_MODIFY_MODE inModifyMode) +{ + modifyDatabase(); + return findTable(inTableId).updateRecord(inRecordId, inAttributes, inData, inModifyMode); +} + +// Create a table associated with a given metarecord, and add the table +// to the database. + +ModifiedTable * +DbModifier::createTable(MetaRecord *inMetaRecord) +{ + auto_ptr aMetaRecord(inMetaRecord); + auto_ptr aModifiedTable(new ModifiedTable(inMetaRecord)); + // Now that aModifiedTable is fully constructed it owns inMetaRecord + aMetaRecord.release(); + + if (!mModifiedTableMap.insert + (ModifiedTableMap::value_type(inMetaRecord->dataRecordType(), + aModifiedTable.get())).second) + { + // XXX Should be CSSMERR_DL_DUPLICATE_RECORDTYPE. Since that + // doesn't exist we report that the metatable's unique index would + // no longer be valid + CssmError::throwMe(CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA); + } + + return aModifiedTable.release(); +} + +void +DbModifier::deleteTable(Table::Id inTableId) +{ + modifyDatabase(); + // Can't delete schema tables. + if (CSSM_DB_RECORDTYPE_SCHEMA_START <= inTableId + && inTableId < CSSM_DB_RECORDTYPE_SCHEMA_END) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORDTYPE); + + // Find the ModifiedTable and delete it + ModifiedTableMap::iterator it = mModifiedTableMap.find(inTableId); + if (it == mModifiedTableMap.end()) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORDTYPE); + + delete it->second; + mModifiedTableMap.erase(it); +} + +uint32 +DbModifier::writeAuthSection(uint32 inSectionOffset) +{ + WriteSection anAuthSection; + + // XXX Put real data into the authsection. + uint32 anOffset = anAuthSection.put(0, 0); + anAuthSection.size(anOffset); + + mAtomicTempFile->write(AtomicFile::FromStart, inSectionOffset, + anAuthSection.address(), anAuthSection.size()); + return inSectionOffset + anOffset; +} + +uint32 +DbModifier::writeSchemaSection(uint32 inSectionOffset) +{ + uint32 aTableCount = mModifiedTableMap.size(); + WriteSection aTableSection(Allocator::standard(), + OffsetTables + AtomSize * aTableCount); + // Set aTableSection to the correct size. + aTableSection.size(OffsetTables + AtomSize * aTableCount); + aTableSection.put(OffsetTablesCount, aTableCount); + + uint32 anOffset = inSectionOffset + OffsetTables + AtomSize * aTableCount; + ModifiedTableMap::const_iterator anIt = mModifiedTableMap.begin(); + ModifiedTableMap::const_iterator anEnd = mModifiedTableMap.end(); + for (uint32 aTableNumber = 0; anIt != anEnd; anIt++, aTableNumber++) + { + // Put the offset to the current table relative to the start of + // this section into the tables array + aTableSection.put(OffsetTables + AtomSize * aTableNumber, + anOffset - inSectionOffset); + anOffset = anIt->second->writeTable(*mAtomicTempFile, anOffset); + } + + aTableSection.put(OffsetSchemaSize, anOffset - inSectionOffset); + mAtomicTempFile->write(AtomicFile::FromStart, inSectionOffset, + aTableSection.address(), aTableSection.size()); + + return anOffset; +} + +void +DbModifier::commit() +{ + if (!mAtomicTempFile) + return; + try + { + WriteSection aHeaderSection(Allocator::standard(), size_t(HeaderSize)); + // Set aHeaderSection to the correct size. + aHeaderSection.size(HeaderSize); + + // Start writing sections after the header + uint32 anOffset = HeaderOffset + HeaderSize; + + // Write auth section + aHeaderSection.put(OffsetAuthOffset, anOffset); + anOffset = writeAuthSection(anOffset); + // Write schema section + aHeaderSection.put(OffsetSchemaOffset, anOffset); + anOffset = writeSchemaSection(anOffset); + + // Write out the file header. + aHeaderSection.put(OffsetMagic, HeaderMagic); + aHeaderSection.put(OffsetVersion, HeaderVersion); + mAtomicTempFile->write(AtomicFile::FromStart, HeaderOffset, + aHeaderSection.address(), aHeaderSection.size()); + + // Write out the versionId. + WriteSection aVersionSection(Allocator::standard(), size_t(AtomSize)); + anOffset = aVersionSection.put(0, mVersionId); + aVersionSection.size(anOffset); + + mAtomicTempFile->write(AtomicFile::FromEnd, 0, + aVersionSection.address(), aVersionSection.size()); + + mAtomicTempFile->commit(); + mAtomicTempFile = NULL; + /* Initialize the shared memory file change mechanism */ + pthread_once(&gCommonInitMutex, initCommon); + + if (gSegment != NULL) + { + /* + PLEASE NOTE: + + The following operation is endian safe because we are not looking + for monotonic increase. I have tested every possible value of + *gSegment, and there is no value for which alternating + big and little endian increments will produce the original value. + */ + + OSAtomicIncrement32Barrier (gSegment); + } + } + catch(...) + { + rollback(); + throw; + } +} + +void +DbModifier::rollback() throw() +{ + // This will destroy the AtomicTempFile if we have one causing it to rollback. + mAtomicTempFile = NULL; +} + +const RecordId +DbModifier::getRecord(Table::Id inTableId, const RecordId &inRecordId, + CSSM_DB_RECORD_ATTRIBUTE_DATA *inoutAttributes, + CssmData *inoutData, Allocator &inAllocator) +{ + if (mAtomicTempFile) + { + // We are in the midst of changing the database. + return findTable(inTableId).getRecord(inRecordId, inoutAttributes, + inoutData, inAllocator); + } + else + { + return getDbVersion(false)->getRecord(inTableId, inRecordId, + inoutAttributes, inoutData, inAllocator); + } +} + +Cursor * +DbModifier::createCursor(const CSSM_QUERY *inQuery) +{ + if (mAtomicTempFile) + { + // We are modifying this database. + + // If we have a mDbVersion already then it's a snapshot of the database + // right before the modifications started. So return a cursor using + // that. + if (mDbVersion) + return mDbVersion->createCursor(inQuery); + + // This is a newly created but never commited database. Return a + // Cursor that will not return any matches. + return new Cursor(); + } + + // Get the latest and greatest version of the db and create the cursor + // on that. + return getDbVersion(false)->createCursor(inQuery); +} + +// Insert schema records for a new table into the metatables of the database. This gets +// called while a database is being created. + +void +DbModifier::insertTableSchema(const CssmDbRecordAttributeInfo &inInfo, + const CSSM_DB_RECORD_INDEX_INFO *inIndexInfo /* = NULL */) +{ + ModifiedTable &aTable = findTable(inInfo.DataRecordType); + const MetaRecord &aMetaRecord = aTable.getMetaRecord(); + + CssmAutoDbRecordAttributeData aRecordBuilder(5); // Set capacity to 5 so we don't need to grow + + // Create the entry for the SchemaRelations table. + aRecordBuilder.add(RelationID, inInfo.recordType()); + aRecordBuilder.add(RelationName, mDb.recordName(inInfo.recordType())); + + // Insert the record into the SchemaRelations ModifiedTable + findTable(mDb.schemaRelations.DataRecordType).insertRecord(mVersionId, + &aRecordBuilder, NULL); + + ModifiedTable &anAttributeTable = findTable(mDb.schemaAttributes.DataRecordType); + for (uint32 anIndex = 0; anIndex < inInfo.size(); anIndex++) + { + // Create an entry for the SchemaAttributes table. + aRecordBuilder.clear(); + aRecordBuilder.add(RelationID, inInfo.recordType()); + aRecordBuilder.add(AttributeNameFormat, inInfo.at(anIndex).nameFormat()); + + uint32 attributeId = aMetaRecord.metaAttribute(inInfo.at(anIndex)).attributeId(); + + switch (inInfo.at(anIndex).nameFormat()) + { + case CSSM_DB_ATTRIBUTE_NAME_AS_STRING: + aRecordBuilder.add(AttributeName, inInfo.at(anIndex).Label.AttributeName); + break; + case CSSM_DB_ATTRIBUTE_NAME_AS_OID: + aRecordBuilder.add(AttributeNameID, inInfo.at(anIndex).Label.AttributeOID); + break; + case CSSM_DB_ATTRIBUTE_NAME_AS_INTEGER: + break; + default: + CssmError::throwMe(CSSMERR_DL_INVALID_FIELD_NAME); + } + + aRecordBuilder.add(AttributeID, attributeId); + aRecordBuilder.add(AttributeFormat, inInfo.at(anIndex).format()); + + // Insert the record into the SchemaAttributes ModifiedTable + anAttributeTable.insertRecord(mVersionId, &aRecordBuilder, NULL); + } + + if (inIndexInfo != NULL) { + + if (inIndexInfo->DataRecordType != inInfo.DataRecordType && + inIndexInfo->NumberOfIndexes > 0) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORDTYPE); + + ModifiedTable &indexMetaTable = findTable(mDb.schemaIndexes.DataRecordType); + uint32 aNumberOfIndexes = inIndexInfo->NumberOfIndexes; + + for (uint32 anIndex = 0; anIndex < aNumberOfIndexes; anIndex++) + { + const CssmDbIndexInfo &thisIndex = CssmDbIndexInfo::overlay(inIndexInfo->IndexInfo[anIndex]); + + // make sure the index is supported + if (thisIndex.dataLocation() != CSSM_DB_INDEX_ON_ATTRIBUTE) + CssmError::throwMe(CSSMERR_DL_INVALID_INDEX_INFO); + + // assign an index ID: the unique index is ID 0, all others are ID > 0 + uint32 indexId; + if (thisIndex.IndexType == CSSM_DB_INDEX_UNIQUE) + indexId = 0; + else + indexId = anIndex + 1; + + // figure out the attribute ID + uint32 attributeId = + aMetaRecord.metaAttribute(thisIndex.Info).attributeId(); + + // Create an entry for the SchemaIndexes table. + aRecordBuilder.clear(); + aRecordBuilder.add(RelationID, inInfo.DataRecordType); + aRecordBuilder.add(IndexID, indexId); + aRecordBuilder.add(AttributeID, attributeId); + aRecordBuilder.add(IndexType, thisIndex.IndexType); + aRecordBuilder.add(IndexedDataLocation, thisIndex.IndexedDataLocation); + + // Insert the record into the SchemaIndexes ModifiedTable + indexMetaTable.insertRecord(mVersionId, &aRecordBuilder, NULL); + + // update the table's index objects + DbMutableIndex &index = aTable.findIndex(indexId, aMetaRecord, indexId == 0); + index.appendAttribute(attributeId); + } + } +} + +// Insert a new table. The attribute info is required; the index and parsing module +// descriptions are optional. This version gets called during the creation of a +// database. + +void +DbModifier::insertTable(const CssmDbRecordAttributeInfo &inInfo, + const CSSM_DB_RECORD_INDEX_INFO *inIndexInfo /* = NULL */, + const CSSM_DB_PARSING_MODULE_INFO *inParsingModule /* = NULL */) +{ + modifyDatabase(); + createTable(new MetaRecord(inInfo)); + insertTableSchema(inInfo, inIndexInfo); +} + +// Insert a new table. This is the version that gets called when a table is added +// after a database has been created. + +void +DbModifier::insertTable(Table::Id inTableId, const string &inTableName, + uint32 inNumberOfAttributes, + const CSSM_DB_SCHEMA_ATTRIBUTE_INFO *inAttributeInfo, + uint32 inNumberOfIndexes, + const CSSM_DB_SCHEMA_INDEX_INFO *inIndexInfo) +{ + modifyDatabase(); + ModifiedTable *aTable = createTable(new MetaRecord(inTableId, inNumberOfAttributes, inAttributeInfo)); + + CssmAutoDbRecordAttributeData aRecordBuilder(6); // Set capacity to 6 so we don't need to grow + + // Create the entry for the SchemaRelations table. + aRecordBuilder.add(RelationID, inTableId); + aRecordBuilder.add(RelationName, inTableName); + + // Insert the record into the SchemaRelations ModifiedTable + findTable(mDb.schemaRelations.DataRecordType).insertRecord(mVersionId, + &aRecordBuilder, NULL); + + ModifiedTable &anAttributeTable = findTable(mDb.schemaAttributes.DataRecordType); + for (uint32 anIndex = 0; anIndex < inNumberOfAttributes; anIndex++) + { + // Create an entry for the SchemaAttributes table. + aRecordBuilder.clear(); + aRecordBuilder.add(RelationID, inTableId); + // XXX What should this be? We set it to CSSM_DB_ATTRIBUTE_NAME_AS_INTEGER for now + // since the AttributeID is always valid. + aRecordBuilder.add(AttributeNameFormat, uint32(CSSM_DB_ATTRIBUTE_NAME_AS_INTEGER)); + aRecordBuilder.add(AttributeID, inAttributeInfo[anIndex].AttributeId); + if (inAttributeInfo[anIndex].AttributeName) + aRecordBuilder.add(AttributeName, inAttributeInfo[anIndex].AttributeName); + if (inAttributeInfo[anIndex].AttributeNameID.Length > 0) + aRecordBuilder.add(AttributeNameID, inAttributeInfo[anIndex].AttributeNameID); + aRecordBuilder.add(AttributeFormat, inAttributeInfo[anIndex].DataType); + + // Insert the record into the SchemaAttributes ModifiedTable + anAttributeTable.insertRecord(mVersionId, &aRecordBuilder, NULL); + } + + ModifiedTable &anIndexTable = findTable(mDb.schemaIndexes.DataRecordType); + for (uint32 anIndex = 0; anIndex < inNumberOfIndexes; anIndex++) + { + // Create an entry for the SchemaIndexes table. + aRecordBuilder.clear(); + aRecordBuilder.add(RelationID, inTableId); + aRecordBuilder.add(IndexID, inIndexInfo[anIndex].IndexId); + aRecordBuilder.add(AttributeID, inIndexInfo[anIndex].AttributeId); + aRecordBuilder.add(IndexType, inIndexInfo[anIndex].IndexType); + aRecordBuilder.add(IndexedDataLocation, inIndexInfo[anIndex].IndexedDataLocation); + + // Insert the record into the SchemaIndexes ModifiedTable + anIndexTable.insertRecord(mVersionId, &aRecordBuilder, NULL); + + // update the table's index objects + DbMutableIndex &index = aTable->findIndex(inIndexInfo[anIndex].IndexId, + aTable->getMetaRecord(), inIndexInfo[anIndex].IndexType == CSSM_DB_INDEX_UNIQUE); + index.appendAttribute(inIndexInfo[anIndex].AttributeId); + } +} + + + +bool DbModifier::hasTable(Table::Id inTableId) +{ + return getDbVersion(false)->hasTable(inTableId); +} + + + +ModifiedTable & +DbModifier::findTable(Table::Id inTableId) +{ + ModifiedTableMap::iterator it = mModifiedTableMap.find(inTableId); + if (it == mModifiedTableMap.end()) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORDTYPE); + return *it->second; +} + + +// +// AppleDatabaseManager implementation +// + +AppleDatabaseManager::AppleDatabaseManager(const AppleDatabaseTableName *tableNames) + : DatabaseManager(), + mTableNames(tableNames) +{ + // make sure that a proper set of table ids and names has been provided + + if (!mTableNames) + CssmError::throwMe(CSSMERR_DL_INTERNAL_ERROR); + else { + uint32 i; + for (i = 0; mTableNames[i].mTableName; i++) {} + if (i < AppleDatabaseTableName::kNumRequiredTableNames) + CssmError::throwMe(CSSMERR_DL_INTERNAL_ERROR); + } +} + +Database * +AppleDatabaseManager::make(const DbName &inDbName) +{ + return new AppleDatabase(inDbName, mTableNames); +} + + +// +// AppleDbContext implementation +// + +/* This is the version 0 CSSM_APPLEDL_OPEN_PARAMETERS struct used up to 10.2.x. */ +extern "C" { + +typedef struct cssm_appledl_open_parameters_v0 +{ + uint32 length; /* Should be sizeof(CSSM_APPLEDL_OPEN_PARAMETERS_V0). */ + uint32 version; /* Should be 0. */ + CSSM_BOOL autoCommit; +} CSSM_APPLEDL_OPEN_PARAMETERS_V0; + +}; + +AppleDbContext::AppleDbContext(Database &inDatabase, + DatabaseSession &inDatabaseSession, + CSSM_DB_ACCESS_TYPE inAccessRequest, + const AccessCredentials *inAccessCred, + const void *inOpenParameters) : + DbContext(inDatabase, inDatabaseSession, inAccessRequest, inAccessCred), + mAutoCommit(true), + mMode(0666) +{ + const CSSM_APPLEDL_OPEN_PARAMETERS *anOpenParameters = + reinterpret_cast(inOpenParameters); + + if (anOpenParameters) + { + switch (anOpenParameters->version) + { + case 1: + if (anOpenParameters->length < sizeof(CSSM_APPLEDL_OPEN_PARAMETERS)) + CssmError::throwMe(CSSMERR_APPLEDL_INVALID_OPEN_PARAMETERS); + + if (anOpenParameters->mask & kCSSM_APPLEDL_MASK_MODE) + mMode = anOpenParameters->mode; + /*DROPTHROUGH*/ + case 0: + if (anOpenParameters->length < sizeof(CSSM_APPLEDL_OPEN_PARAMETERS_V0)) + CssmError::throwMe(CSSMERR_APPLEDL_INVALID_OPEN_PARAMETERS); + + mAutoCommit = anOpenParameters->autoCommit == CSSM_FALSE ? false : true; + break; + + default: + CssmError::throwMe(CSSMERR_APPLEDL_INVALID_OPEN_PARAMETERS); + } + } +} + +AppleDbContext::~AppleDbContext() +{ +} + +// +// AppleDatabase implementation +// +AppleDatabase::AppleDatabase(const DbName &inDbName, const AppleDatabaseTableName *tableNames) : + Database(inDbName), + schemaRelations(tableNames[AppleDatabaseTableName::kSchemaInfo].mTableId, + sizeof(AttrSchemaRelations) / sizeof(CSSM_DB_ATTRIBUTE_INFO), + const_cast(AttrSchemaRelations)), + schemaAttributes(tableNames[AppleDatabaseTableName::kSchemaAttributes].mTableId, + sizeof(AttrSchemaAttributes) / sizeof(CSSM_DB_ATTRIBUTE_INFO), + const_cast(AttrSchemaAttributes)), + schemaIndexes(tableNames[AppleDatabaseTableName::kSchemaIndexes].mTableId, + sizeof(AttrSchemaIndexes) / sizeof(CSSM_DB_ATTRIBUTE_INFO), + const_cast(AttrSchemaIndexes)), + schemaParsingModule(tableNames[AppleDatabaseTableName::kSchemaParsingModule].mTableId, + sizeof(AttrSchemaParsingModule) / sizeof(CSSM_DB_ATTRIBUTE_INFO), + const_cast(AttrSchemaParsingModule)), + mAtomicFile(mDbName.dbName()), + mDbModifier(mAtomicFile, *this), + mTableNames(tableNames) +{ + /* temp check for X509Anchors access - this should removed before Leopard GM */ + if(!strcmp(inDbName.dbName(), "/System/Library/Keychains/X509Anchors")) { + Syslog::alert("Warning: accessing obsolete X509Anchors."); + } +} + +AppleDatabase::~AppleDatabase() +{ +} + +// Return the name of a record type. This uses a table that maps record types +// to record names. The table is provided when the database is created. + +const char *AppleDatabase::recordName(CSSM_DB_RECORDTYPE inRecordType) const +{ + if (inRecordType == CSSM_DL_DB_RECORD_ANY || inRecordType == CSSM_DL_DB_RECORD_ALL_KEYS) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORDTYPE); + + for (uint32 i = 0; mTableNames[i].mTableName; i++) + if (mTableNames[i].mTableId == inRecordType) + return mTableNames[i].mTableName; + + return ""; +} + +DbContext * +AppleDatabase::makeDbContext(DatabaseSession &inDatabaseSession, + CSSM_DB_ACCESS_TYPE inAccessRequest, + const AccessCredentials *inAccessCred, + const void *inOpenParameters) +{ + return new AppleDbContext(*this, inDatabaseSession, inAccessRequest, + inAccessCred, inOpenParameters); +} + +void +AppleDatabase::dbCreate(DbContext &inDbContext, const CSSM_DBINFO &inDBInfo, + const CSSM_ACL_ENTRY_INPUT *inInitialAclEntry) +{ + AppleDbContext &context = safer_cast(inDbContext); + try + { + StLock _(mWriteLock); + mDbModifier.createDatabase(inDBInfo, inInitialAclEntry, context.mode()); + } + catch(...) + { + mDbModifier.rollback(); + throw; + } + if (context.autoCommit()) + mDbModifier.commit(); +} + +void +AppleDatabase::dbOpen(DbContext &inDbContext) +{ + mDbModifier.openDatabase(); +} + +void +AppleDatabase::dbClose() +{ + StLock _(mWriteLock); + mDbModifier.closeDatabase(); +} + +void +AppleDatabase::dbDelete(DatabaseSession &inDatabaseSession, + const AccessCredentials *inAccessCred) +{ + StLock _(mWriteLock); + // XXX Check callers credentials. + mDbModifier.deleteDatabase(); +} + +void +AppleDatabase::createRelation(DbContext &inDbContext, + CSSM_DB_RECORDTYPE inRelationID, + const char *inRelationName, + uint32 inNumberOfAttributes, + const CSSM_DB_SCHEMA_ATTRIBUTE_INFO *inAttributeInfo, + uint32 inNumberOfIndexes, + const CSSM_DB_SCHEMA_INDEX_INFO &inIndexInfo) +{ + try + { + StLock _(mWriteLock); + // XXX Fix the refs here. + mDbModifier.insertTable(inRelationID, inRelationName, + inNumberOfAttributes, inAttributeInfo, + inNumberOfIndexes, &inIndexInfo); + } + catch(...) + { + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.rollback(); + throw; + } + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.commit(); +} + +void +AppleDatabase::destroyRelation(DbContext &inDbContext, + CSSM_DB_RECORDTYPE inRelationID) +{ + try + { + StLock _(mWriteLock); + mDbModifier.deleteTable(inRelationID); + } + catch(...) + { + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.rollback(); + throw; + } + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.commit(); +} + +void +AppleDatabase::authenticate(DbContext &inDbContext, + CSSM_DB_ACCESS_TYPE inAccessRequest, + const AccessCredentials &inAccessCred) +{ + CssmError::throwMe(CSSM_ERRCODE_FUNCTION_NOT_IMPLEMENTED); +} + +void +AppleDatabase::getDbAcl(DbContext &inDbContext, + const CSSM_STRING *inSelectionTag, + uint32 &outNumberOfAclInfos, + CSSM_ACL_ENTRY_INFO_PTR &outAclInfos) +{ + CssmError::throwMe(CSSM_ERRCODE_FUNCTION_NOT_IMPLEMENTED); +} + +void +AppleDatabase::changeDbAcl(DbContext &inDbContext, + const AccessCredentials &inAccessCred, + const CSSM_ACL_EDIT &inAclEdit) +{ + CssmError::throwMe(CSSM_ERRCODE_FUNCTION_NOT_IMPLEMENTED); +} + +void +AppleDatabase::getDbOwner(DbContext &inDbContext, + CSSM_ACL_OWNER_PROTOTYPE &outOwner) +{ + CssmError::throwMe(CSSM_ERRCODE_FUNCTION_NOT_IMPLEMENTED); +} + +void +AppleDatabase::changeDbOwner(DbContext &inDbContext, + const AccessCredentials &inAccessCred, + const CSSM_ACL_OWNER_PROTOTYPE &inNewOwner) +{ + CssmError::throwMe(CSSM_ERRCODE_FUNCTION_NOT_IMPLEMENTED); +} + +char * +AppleDatabase::getDbNameFromHandle(const DbContext &inDbContext) const +{ + CssmError::throwMe(CSSM_ERRCODE_FUNCTION_NOT_IMPLEMENTED); +} + +CSSM_DB_UNIQUE_RECORD_PTR +AppleDatabase::dataInsert(DbContext &inDbContext, + CSSM_DB_RECORDTYPE inRecordType, + const CSSM_DB_RECORD_ATTRIBUTE_DATA *inAttributes, + const CssmData *inData) +{ + CSSM_DB_UNIQUE_RECORD_PTR anUniqueRecordPtr = NULL; + try + { + StLock _(mWriteLock); + const RecordId aRecordId = + mDbModifier.insertRecord(inRecordType, inAttributes, inData); + + anUniqueRecordPtr = createUniqueRecord(inDbContext, inRecordType, + aRecordId); + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.commit(); + } + catch(...) + { + if (anUniqueRecordPtr != NULL) + freeUniqueRecord(inDbContext, *anUniqueRecordPtr); + + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.rollback(); + throw; + } + + return anUniqueRecordPtr; +} + +void +AppleDatabase::dataDelete(DbContext &inDbContext, + const CSSM_DB_UNIQUE_RECORD &inUniqueRecord) +{ + try + { + // syslog if it's the .Mac password + CSSM_DB_RECORD_ATTRIBUTE_DATA attrData; + // we have to do this in two phases -- the first to get the record type, and the second to actually read the attributes. Otherwise, we might get + // an exception. + memset(&attrData, 0, sizeof(attrData)); + dataGetFromUniqueRecordId(inDbContext, inUniqueRecord, &attrData, NULL); + + if (attrData.DataRecordType == CSSM_DL_DB_RECORD_GENERIC_PASSWORD) + { + CSSM_DB_ATTRIBUTE_DATA attributes; + + // setup some attributes and see if we are indeed the .Mac password + attributes.Info.AttributeNameFormat = CSSM_DB_ATTRIBUTE_NAME_AS_INTEGER; + attributes.Info.Label.AttributeID = 'svce'; + attributes.Info.AttributeFormat = 0; + attributes.NumberOfValues = 1; + attributes.Value = NULL; + + attrData.NumberOfAttributes = 1; + attrData.AttributeData = &attributes; + + dataGetFromUniqueRecordId(inDbContext, inUniqueRecord, &attrData, NULL); + + // now check the results + std::string dataString((const char*) attrData.AttributeData[0].Value[0].Data, attrData.AttributeData[0].Value[0].Length); + if (dataString == "iTools") + { + syslog(LOG_WARNING, "Warning: Removed .Me password"); + } + + free(attrData.AttributeData[0].Value[0].Data); + free(attrData.AttributeData[0].Value); + } + + StLock _(mWriteLock); + Table::Id aTableId; + const RecordId aRecordId(parseUniqueRecord(inUniqueRecord, aTableId)); + mDbModifier.deleteRecord(aTableId, aRecordId); + } + catch(...) + { + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.rollback(); + throw; + } + + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.commit(); +} + +void +AppleDatabase::dataModify(DbContext &inDbContext, + CSSM_DB_RECORDTYPE inRecordType, + CSSM_DB_UNIQUE_RECORD &inoutUniqueRecord, + const CSSM_DB_RECORD_ATTRIBUTE_DATA *inAttributesToBeModified, + const CssmData *inDataToBeModified, + CSSM_DB_MODIFY_MODE inModifyMode) +{ + try + { + StLock _(mWriteLock); + Table::Id aTableId; + const RecordId oldRecordId = parseUniqueRecord(inoutUniqueRecord, + aTableId); +#if 1 + if (inRecordType != aTableId) +#else + if (inRecordType != aTableId && + inRecordType != CSSM_DL_DB_RECORD_ANY && + !(inRecordType == CSSM_DL_DB_RECORD_ALL_KEYS && + (aTableId == CSSM_DL_DB_RECORD_PUBLIC_KEY || + aTableId == CSSM_DL_DB_RECORD_PRIVATE_KEY || + aTableId == CSSM_DL_DB_RECORD_SYMMETRIC_KEY))) +#endif + { + CssmError::throwMe(CSSMERR_DL_INVALID_RECORD_UID); + } + + const RecordId newRecordId = + mDbModifier.updateRecord(aTableId, + oldRecordId, + inAttributesToBeModified, + inDataToBeModified, + inModifyMode); + updateUniqueRecord(inDbContext, aTableId, newRecordId, + inoutUniqueRecord); + } + catch(...) + { + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.rollback(); + throw; + } + + if (safer_cast(inDbContext).autoCommit()) + mDbModifier.commit(); +} + +CSSM_HANDLE +AppleDatabase::dataGetFirst(DbContext &inDbContext, + const CssmQuery *inQuery, + CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR inoutAttributes, + CssmData *inoutData, + CSSM_DB_UNIQUE_RECORD_PTR &outUniqueRecord) +{ + // XXX: register Cursor with DbContext and have DbContext call + // dataAbortQuery for all outstanding Query objects on close. + auto_ptr aCursor(mDbModifier.createCursor(inQuery)); + Table::Id aTableId; + RecordId aRecordId; + + if (!aCursor->next(aTableId, inoutAttributes, inoutData, + inDbContext.mDatabaseSession, aRecordId)) + // return a NULL handle, and implicitly delete the cursor + return CSSM_INVALID_HANDLE; + + outUniqueRecord = createUniqueRecord(inDbContext, aTableId, aRecordId); + return aCursor.release()->handle(); // We didn't throw so keep the Cursor around. +} + +bool +AppleDatabase::dataGetNext(DbContext &inDbContext, + CSSM_HANDLE inResultsHandle, + CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR inoutAttributes, + CssmData *inoutData, + CSSM_DB_UNIQUE_RECORD_PTR &outUniqueRecord) +{ + auto_ptr aCursor(&HandleObject::find(inResultsHandle, CSSMERR_DL_INVALID_RESULTS_HANDLE)); + Table::Id aTableId; + RecordId aRecordId; + + if (!aCursor->next(aTableId, inoutAttributes, inoutData, inDbContext.mDatabaseSession, aRecordId)) + return false; + + outUniqueRecord = createUniqueRecord(inDbContext, aTableId, aRecordId); + + aCursor.release(); + return true; +} + +void +AppleDatabase::dataAbortQuery(DbContext &inDbContext, + CSSM_HANDLE inResultsHandle) +{ + delete &HandleObject::find(inResultsHandle, CSSMERR_DL_INVALID_RESULTS_HANDLE); +} + +void +AppleDatabase::dataGetFromUniqueRecordId(DbContext &inDbContext, + const CSSM_DB_UNIQUE_RECORD &inUniqueRecord, + CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR inoutAttributes, + CssmData *inoutData) +{ + Table::Id aTableId; + const RecordId aRecordId(parseUniqueRecord(inUniqueRecord, aTableId)); + // XXX Change CDSA spec to use new RecordId returned by this function + mDbModifier.getRecord(aTableId, aRecordId, inoutAttributes, inoutData, + inDbContext.mDatabaseSession); +} + +void +AppleDatabase::freeUniqueRecord(DbContext &inDbContext, + CSSM_DB_UNIQUE_RECORD &inUniqueRecord) +{ + if (inUniqueRecord.RecordIdentifier.Length != 0 + && inUniqueRecord.RecordIdentifier.Data != NULL) + { + inUniqueRecord.RecordIdentifier.Length = 0; + inDbContext.mDatabaseSession.free(inUniqueRecord.RecordIdentifier.Data); + } + inDbContext.mDatabaseSession.free(&inUniqueRecord); +} + +void +AppleDatabase::updateUniqueRecord(DbContext &inDbContext, + CSSM_DB_RECORDTYPE inTableId, + const RecordId &inRecordId, + CSSM_DB_UNIQUE_RECORD &inoutUniqueRecord) +{ + uint32 *aBuffer = reinterpret_cast(inoutUniqueRecord.RecordIdentifier.Data); + aBuffer[0] = inTableId; + aBuffer[1] = inRecordId.mRecordNumber; + aBuffer[2] = inRecordId.mCreateVersion; + aBuffer[3] = inRecordId.mRecordVersion; +} + +CSSM_DB_UNIQUE_RECORD_PTR +AppleDatabase::createUniqueRecord(DbContext &inDbContext, + CSSM_DB_RECORDTYPE inTableId, + const RecordId &inRecordId) +{ + CSSM_DB_UNIQUE_RECORD_PTR aUniqueRecord = + inDbContext.mDatabaseSession.alloc(); + memset(aUniqueRecord, 0, sizeof(*aUniqueRecord)); + aUniqueRecord->RecordIdentifier.Length = sizeof(uint32) * 4; + try + { + aUniqueRecord->RecordIdentifier.Data = + inDbContext.mDatabaseSession.alloc(sizeof(uint32) * 4); + updateUniqueRecord(inDbContext, inTableId, inRecordId, *aUniqueRecord); + } + catch(...) + { + inDbContext.mDatabaseSession.free(aUniqueRecord); + throw; + } + + return aUniqueRecord; +} + +const RecordId +AppleDatabase::parseUniqueRecord(const CSSM_DB_UNIQUE_RECORD &inUniqueRecord, + CSSM_DB_RECORDTYPE &outTableId) +{ + if (inUniqueRecord.RecordIdentifier.Length != sizeof(uint32) * 4) + CssmError::throwMe(CSSMERR_DL_INVALID_RECORD_UID); + + uint32 *aBuffer = reinterpret_cast(inUniqueRecord.RecordIdentifier.Data); + outTableId = aBuffer[0]; + return RecordId(aBuffer[1], aBuffer[2], aBuffer[3]); +} + +void +AppleDatabase::passThrough(DbContext &dbContext, + uint32 passThroughId, + const void *inputParams, + void **outputParams) +{ + switch (passThroughId) + { + case CSSM_APPLEFILEDL_TOGGLE_AUTOCOMMIT: + { + AppleDbContext &dbc = safer_cast(dbContext); + // Return the old state of the autoCommit flag if requested + if (outputParams) + *reinterpret_cast(outputParams) = dbc.autoCommit(); + dbc.autoCommit(inputParams ? CSSM_TRUE : CSSM_FALSE); + } + break; + + case CSSM_APPLEFILEDL_COMMIT: + mDbModifier.commit(); + break; + + case CSSM_APPLEFILEDL_ROLLBACK: + mDbModifier.rollback(); + break; + + case CSSM_APPLECSPDL_DB_RELATION_EXISTS: + { + CSSM_BOOL returnValue; + + CSSM_DB_RECORDTYPE recordType = *(CSSM_DB_RECORDTYPE*) inputParams; + if (recordType == CSSM_DL_DB_RECORD_ANY || recordType == CSSM_DL_DB_RECORD_ALL_KEYS) + { + returnValue = CSSM_TRUE; + } + else + { + returnValue = mDbModifier.hasTable(recordType); + } + + *(CSSM_BOOL*) outputParams = returnValue; + break; + } + + default: + CssmError::throwMe(CSSM_ERRCODE_FUNCTION_NOT_IMPLEMENTED); + break; + } +}