/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
using CacheIndexHeader = struct { // Version of the index. The index must be ignored and deleted when the file // on disk was written with a newer version.
uint32_t mVersion;
// Timestamp of time when the last successful write of the index started. // During update process we use this timestamp for a quick validation of entry // files. If last modified time of the file is lower than this timestamp, we // skip parsing of such file since the information in index should be up to // date.
uint32_t mTimeStamp;
// We set this flag as soon as possible after parsing index during startup // and clean it after we write journal to disk during shutdown. We ignore the // journal and start update process whenever this flag is set during index // parsing.
uint32_t mIsDirty;
// The amount of data written to the cache. When it reaches // kTelemetryReportBytesLimit a telemetry report is sent and the counter is // reset.
uint32_t mKBWritten;
};
class CacheIndexRecordWrapper final { public:
NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DESTROY(
CacheIndexRecordWrapper, DispatchDeleteSelfToCurrentThread());
class CacheIndexEntry : public PLDHashEntryHdr { public: using KeyType = const SHA1Sum::Hash&; using KeyTypePointer = const SHA1Sum::Hash*;
explicit CacheIndexEntry(KeyTypePointer aKey) {
MOZ_COUNT_CTOR(CacheIndexEntry);
mRec = new CacheIndexRecordWrapper();
LOG(("CacheIndexEntry::CacheIndexEntry() - Created record [rec=%p]",
mRec->Get()));
memcpy(&mRec->Get()->mHash, aKey, sizeof(SHA1Sum::Hash));
}
CacheIndexEntry(const CacheIndexEntry& aOther) {
MOZ_ASSERT_UNREACHABLE("CacheIndexEntry copy constructor is forbidden!");
}
~CacheIndexEntry() {
MOZ_COUNT_DTOR(CacheIndexEntry);
LOG(("CacheIndexEntry::~CacheIndexEntry() - Deleting record [rec=%p]",
mRec->Get()));
}
// KeyEquals(): does this entry match this key? bool KeyEquals(KeyTypePointer aKey) const { return memcmp(&mRec->Get()->mHash, aKey, sizeof(SHA1Sum::Hash)) == 0;
}
void Init(OriginAttrsHash aOriginAttrsHash, bool aAnonymous, bool aPinned) {
MOZ_ASSERT(mRec->Get()->mFrecency == 0);
MOZ_ASSERT(mRec->Get()->mOriginAttrsHash == 0);
MOZ_ASSERT(mRec->Get()->mOnStartTime == kIndexTimeNotAvailable);
MOZ_ASSERT(mRec->Get()->mOnStopTime == kIndexTimeNotAvailable);
MOZ_ASSERT(mRec->Get()->mContentType ==
nsICacheEntry::CONTENT_TYPE_UNKNOWN); // When we init the entry it must be fresh and may be dirty
MOZ_ASSERT((mRec->Get()->mFlags & ~kDirtyMask) == kFreshMask);
// This flag is set when the entry was removed. We need to keep this // information in memory until we write the index file. staticconst uint32_t kRemovedMask = 0x20000000;
// This flag is set when the information in memory is not in sync with the // information in index file on disk. staticconst uint32_t kDirtyMask = 0x10000000;
// This flag is set when the information about the entry is fresh, i.e. // we've created or opened this entry during this session, or we've seen // this entry during update or build process. staticconst uint32_t kFreshMask = 0x08000000;
// Indicates a pinned entry. staticconst uint32_t kPinnedMask = 0x04000000;
// Indicates there is cached alternative data in the entry. staticconst uint32_t kHasAltDataMask = 0x02000000; staticconst uint32_t kReservedMask = 0x01000000;
// FileSize in kilobytes staticconst uint32_t kFileSizeMask = 0x00FFFFFF;
void AfterChange(const CacheIndexEntry* aEntry) {
MOZ_ASSERT(mStateLogged, "CacheIndexStats::AfterChange() - state not " "logged!"); #ifdef DEBUG
mStateLogged = false; #endif if (aEntry) {
uint8_t contentType = aEntry->GetContentType();
++mCount;
++mCountByType[contentType]; if (aEntry->IsDirty()) {
mDirty++;
} if (aEntry->IsFresh()) {
mFresh++;
} if (aEntry->IsRemoved()) {
mRemoved++;
} else { if (!aEntry->IsInitialized()) {
mNotInitialized++;
} else { if (aEntry->IsFileEmpty()) {
mEmpty++;
} else {
mSize += aEntry->GetFileSize();
mSizeByType[contentType] += aEntry->GetFileSize();
}
}
}
}
#ifdef DEBUG_STATS if (!mDisableLogging) {
LOG(("CacheIndexStats::AfterChange()"));
Log();
} #endif
}
private:
uint32_t mCount{0};
uint32_t mCountByType[nsICacheEntry::CONTENT_TYPE_LAST]{0};
uint32_t mNotInitialized{0};
uint32_t mRemoved{0};
uint32_t mDirty{0};
uint32_t mFresh{0};
uint32_t mEmpty{0};
uint32_t mSize{0};
uint32_t mSizeByType[nsICacheEntry::CONTENT_TYPE_LAST]{0}; #ifdef DEBUG // We completely remove the data about an entry from the stats in // BeforeChange() and set this flag to true. The entry is then modified, // deleted or created and the data is again put into the stats and this flag // set to false. Statistics must not be read during this time since the // information is not correct. bool mStateLogged{false};
// Disables logging in this instance of CacheIndexStats bool mDisableLogging{false}; #endif
};
class CacheIndex final : public CacheFileIOListener, public nsIRunnable { public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIRUNNABLE
// Following methods can be called only on IO thread.
// Add entry to the index. The entry shouldn't be present in index. This // method is called whenever a new handle for a new entry file is created. The // newly created entry is not initialized and it must be either initialized // with InitEntry() or removed with RemoveEntry(). static nsresult AddEntry(const SHA1Sum::Hash* aHash);
// Inform index about an existing entry that should be present in index. This // method is called whenever a new handle for an existing entry file is // created. Like in case of AddEntry(), either InitEntry() or RemoveEntry() // must be called on the entry, since the entry is not initizlized if the // index is outdated. static nsresult EnsureEntryExists(const SHA1Sum::Hash* aHash);
// Initialize the entry. It MUST be present in index. Call to AddEntry() or // EnsureEntryExists() must precede the call to this method. static nsresult InitEntry(const SHA1Sum::Hash* aHash,
OriginAttrsHash aOriginAttrsHash, bool aAnonymous, bool aPinned);
// Remove entry from index. The entry should be present in index. static nsresult RemoveEntry(const SHA1Sum::Hash* aHash);
// Update some information in entry. The entry MUST be present in index and // MUST be initialized. Call to AddEntry() or EnsureEntryExists() and to // InitEntry() must precede the call to this method. // Pass nullptr if the value didn't change. static nsresult UpdateEntry(const SHA1Sum::Hash* aHash, const uint32_t* aFrecency, constbool* aHasAltData, const uint16_t* aOnStartTime, const uint16_t* aOnStopTime, const uint8_t* aContentType, const uint32_t* aSize);
// Remove all entries from the index. Called when clearing the whole cache. static nsresult RemoveAll();
// Returns status of the entry in index for the given key. It can be called // on any thread. // If the optional aCB callback is given, the it will be called with a // CacheIndexEntry only if _retval is EXISTS when the method returns. static nsresult HasEntry( const nsACString& aKey, EntryStatus* _retval, const std::function<void(const CacheIndexEntry*)>& aCB = nullptr); static nsresult HasEntry( const SHA1Sum::Hash& hash, EntryStatus* _retval, const std::function<void(const CacheIndexEntry*)>& aCB = nullptr);
// Returns a hash of the least important entry that should be evicted if the // cache size is over limit and also returns a total number of all entries in // the index minus the number of forced valid entries and unpinned entries // that we encounter when searching (see below) static nsresult GetEntryForEviction(bool aIgnoreEmptyEntries,
SHA1Sum::Hash* aHash, uint32_t* aCnt);
// Checks if a cache entry is currently forced valid. Used to prevent an entry // (that has been forced valid) from being evicted when the cache size reaches // its limit. staticbool IsForcedValidEntry(const SHA1Sum::Hash* aHash);
// Returns cache size in kB. static nsresult GetCacheSize(uint32_t* _retval);
// Returns number of entry files in the cache static nsresult GetEntryFileCount(uint32_t* _retval);
// Synchronously returns the disk occupation and number of entries // per-context. Callable on any thread. It will ignore loadContextInfo and get // stats for all entries if the aInfo is a nullptr. static nsresult GetCacheStats(nsILoadContextInfo* aInfo, uint32_t* aSize,
uint32_t* aCount);
// Asynchronously gets the disk cache size, used for display in the UI. static nsresult AsyncGetDiskConsumption(
nsICacheStorageConsumptionObserver* aObserver);
// Returns an iterator that returns entries matching a given context that were // present in the index at the time this method was called. If aAddNew is true // then the iterator will also return entries created after this call. // NOTE: When some entry is removed from index it is removed also from the // iterator regardless what aAddNew was passed. static nsresult GetIterator(nsILoadContextInfo* aInfo, bool aAddNew,
CacheIndexIterator** _retval);
// Returns true if we _think_ that the index is up to date. I.e. the state is // READY or WRITING and mIndexNeedsUpdate as well as mShuttingDown is false. static nsresult IsUpToDate(bool* _retval);
// Called from CacheStorageService::Clear() and // CacheFileContextEvictor::EvictEntries(), sets a flag that blocks // notification to AsyncGetDiskConsumption. staticvoid OnAsyncEviction(bool aEvicting);
// We keep track of total bytes written to the cache to be able to do // a telemetry report after writting certain amount of data to the cache. staticvoid UpdateTotalBytesWritten(uint32_t aBytesWritten);
// This method returns false when index is not initialized or is shut down. bool IsIndexUsable() MOZ_REQUIRES(sLock);
// This method checks whether the entry has the same values of // originAttributes and isAnonymous. We don't expect to find a collision // since these values are part of the key that we hash and we use a strong // hash function. staticbool IsCollision(CacheIndexEntry* aEntry,
OriginAttrsHash aOriginAttrsHash, bool aAnonymous);
// Checks whether any of the information about the entry has changed. staticbool HasEntryChanged(CacheIndexEntry* aEntry, const uint32_t* aFrecency, constbool* aHasAltData, const uint16_t* aOnStartTime, const uint16_t* aOnStopTime, const uint8_t* aContentType, const uint32_t* aSize);
// Merge all pending operations from mPendingUpdates into mIndex. void ProcessPendingOperations(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock);
// Following methods perform writing of the index file. // // The index is written periodically, but not earlier than once in // kMinDumpInterval and there must be at least kMinUnwrittenChanges // differences between index on disk and in memory. Index is always first // written to a temporary file and the old index file is replaced when the // writing process succeeds. // // Starts writing of index when both limits (minimal delay between writes and // minimum number of changes in index) were exceeded. bool WriteIndexToDiskIfNeeded(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Starts writing of index file. void WriteIndexToDisk(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Serializes part of mIndex hashtable to the write buffer a writes the buffer // to the file. void WriteRecords(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Finalizes writing process. void FinishWrite(bool aSucceeded, const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock);
// Following methods perform writing of the journal during shutdown. All these // methods must be called only during shutdown since they write/delete files // directly on the main thread instead of using CacheFileIOManager that does // it asynchronously on IO thread. Journal contains only entries that are // dirty, i.e. changes that are not present in the index file on the disk. // When the log is written successfully, the dirty flag in index file is // cleared.
nsresult GetFile(const nsACString& aName, nsIFile** _retval); void RemoveFile(const nsACString& aName) MOZ_REQUIRES(sLock); void RemoveAllIndexFiles() MOZ_REQUIRES(sLock); void RemoveJournalAndTempFile() MOZ_REQUIRES(sLock); // Writes journal to the disk and clears dirty flag in index header.
nsresult WriteLogToDisk() MOZ_REQUIRES(sLock);
// Following methods perform reading of the index from the disk. // // Index is read at startup just after initializing the CacheIndex. There are // 3 files used when manipulating with index: index file, journal file and // a temporary file. All files contain the hash of the data, so we can check // whether the content is valid and complete. Index file contains also a dirty // flag in the index header which is unset on a clean shutdown. During opening // and reading of the files we determine the status of the whole index from // the states of the separate files. Following table shows all possible // combinations: // // index, journal, tmpfile // M * * - index is missing -> BUILD // I * * - index is invalid -> BUILD // D * * - index is dirty -> UPDATE // C M * - index is dirty -> UPDATE // C I * - unexpected state -> UPDATE // C V E - unexpected state -> UPDATE // C V M - index is up to date -> READY // // where the letters mean: // * - any state // E - file exists // M - file is missing // I - data is invalid (parsing failed or hash didn't match) // D - dirty (data in index file is correct, but dirty flag is set) // C - clean (index file is clean) // V - valid (data in journal file is correct) // // Note: We accept the data from journal only when the index is up to date as // a whole (i.e. C,V,M state). // // We rename the journal file to the temporary file as soon as possible after // initial test to ensure that we start update process on the next startup if // FF crashes during parsing of the index. // // Initiates reading index from disk. void ReadIndexFromDisk(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Starts reading data from index file. void StartReadingIndex(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Parses data read from index file. void ParseRecords(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Starts reading data from journal file. void StartReadingJournal(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Parses data read from journal file. void ParseJournal(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Merges entries from journal into mIndex. void MergeJournal(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // In debug build this method checks that we have no fresh entry in mIndex // after we finish reading index and before we process pending operations. void EnsureNoFreshEntry() MOZ_REQUIRES(sLock); // In debug build this method is called after processing pending operations // to make sure mIndexStats contains correct information. void EnsureCorrectStats() MOZ_REQUIRES(sLock);
// Following methods perform updating and building of the index. // Timer callback that starts update or build process. staticvoid DelayedUpdate(nsITimer* aTimer, void* aClosure); void DelayedUpdateLocked(const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Posts timer event that start update or build process.
nsresult ScheduleUpdateTimer(uint32_t aDelay) MOZ_REQUIRES(sLock);
nsresult SetupDirectoryEnumerator() MOZ_REQUIRES(sLock);
nsresult InitEntryFromDiskData(CacheIndexEntry* aEntry,
CacheFileMetadata* aMetaData,
int64_t aFileSize); // Returns true when either a timer is scheduled or event is posted. bool IsUpdatePending() MOZ_REQUIRES(sLock); // Iterates through all files in entries directory that we didn't create/open // during this session, parses them and adds the entries to the index. void BuildIndex(const StaticMutexAutoLock& aProofOfLock) MOZ_REQUIRES(sLock);
bool StartUpdatingIndexIfNeeded(const StaticMutexAutoLock& aProofOfLock, bool aSwitchingToReadyState = false); // Starts update or build process or fires a timer when it is too early after // startup. void StartUpdatingIndex(bool aRebuild, const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock); // Iterates through all files in entries directory that we didn't create/open // during this session and theirs last modified time is newer than timestamp // in the index header. Parses the files and adds the entries to the index. void UpdateIndex(const StaticMutexAutoLock& aProofOfLock) MOZ_REQUIRES(sLock); // Finalizes update or build process. void FinishUpdate(bool aSucceeded, const StaticMutexAutoLock& aProofOfLock)
MOZ_REQUIRES(sLock);
enum EState { // Initial state in which the index is not usable // Possible transitions: // -> READING
INITIAL = 0,
// Index is being read from the disk. // Possible transitions: // -> INITIAL - We failed to dispatch a read event. // -> BUILDING - No or corrupted index file was found. // -> UPDATING - No or corrupted journal file was found. // - Dirty flag was set in index header. // -> READY - Index was read successfully or was interrupted by // pre-shutdown. // -> SHUTDOWN - This could happen only in case of pre-shutdown failure.
READING = 1,
// Index is being written to the disk. // Possible transitions: // -> READY - Writing of index finished or was interrupted by // pre-shutdown.. // -> UPDATING - Writing of index finished, but index was found outdated // during writing. // -> SHUTDOWN - This could happen only in case of pre-shutdown failure.
WRITING = 2,
// Index is being build. // Possible transitions: // -> READY - Building of index finished or was interrupted by // pre-shutdown. // -> SHUTDOWN - This could happen only in case of pre-shutdown failure.
BUILDING = 3,
// Index is being updated. // Possible transitions: // -> READY - Updating of index finished or was interrupted by // pre-shutdown. // -> SHUTDOWN - This could happen only in case of pre-shutdown failure.
UPDATING = 4,
// Index is ready. // Possible transitions: // -> UPDATING - Index was found outdated. // -> SHUTDOWN - Index is shutting down.
READY = 5,
// sLock guards almost everything here... // Also guards FileOpenHelper::mCanceled static StaticMutex sLock;
nsCOMPtr<nsIFile> mCacheDirectory;
EState mState MOZ_GUARDED_BY(sLock){INITIAL}; // Timestamp of time when the index was initialized. We use it to delay // initial update or build of index.
TimeStamp mStartTime MOZ_GUARDED_BY(sLock); // Set to true in PreShutdown(), it is checked on variaous places to prevent // starting any process (write, update, etc.) during shutdown. bool mShuttingDown MOZ_GUARDED_BY(sLock){false}; // When set to true, update process should start as soon as possible. This // flag is set whenever we find some inconsistency which would be fixed by // update process. The flag is checked always when switching to READY state. // To make sure we start the update process as soon as possible, methods that // set this flag should also call StartUpdatingIndexIfNeeded() to cover the // case when we are currently in READY state. bool mIndexNeedsUpdate MOZ_GUARDED_BY(sLock){false}; // Set at the beginning of RemoveAll() which clears the whole index. When // removing all entries we must stop any pending reading, writing, updating or // building operation. This flag is checked at various places and it prevents // we won't start another operation (e.g. canceling reading of the index would // normally start update or build process) bool mRemovingAll MOZ_GUARDED_BY(sLock){false}; // Whether the index file on disk exists and is valid. bool mIndexOnDiskIsValid MOZ_GUARDED_BY(sLock){false}; // When something goes wrong during updating or building process, we don't // mark index clean (and also don't write journal) to ensure that update or // build will be initiated on the next start. bool mDontMarkIndexClean MOZ_GUARDED_BY(sLock){false}; // Timestamp value from index file. It is used during update process to skip // entries that were last modified before this timestamp.
uint32_t mIndexTimeStamp MOZ_GUARDED_BY(sLock){0}; // Timestamp of last time the index was dumped to disk. // NOTE: The index might not be necessarily dumped at this time. The value // is used to schedule next dump of the index.
TimeStamp mLastDumpTime MOZ_GUARDED_BY(sLock);
// Timer of delayed update/build.
nsCOMPtr<nsITimer> mUpdateTimer MOZ_GUARDED_BY(sLock); // True when build or update event is posted bool mUpdateEventPending MOZ_GUARDED_BY(sLock){false};
// Helper members used when reading/writing index from/to disk. // Contains number of entries that should be skipped: // - in hashtable when writing index because they were already written // - in index file when reading index because they were already read
uint32_t mSkipEntries MOZ_GUARDED_BY(sLock){0}; // Number of entries that should be written to disk. This is number of entries // in hashtable that are initialized and are not marked as removed when // writing begins.
uint32_t mProcessEntries MOZ_GUARDED_BY(sLock){0}; char* mRWBuf MOZ_GUARDED_BY(sLock){nullptr};
uint32_t mRWBufSize MOZ_GUARDED_BY(sLock){0};
uint32_t mRWBufPos MOZ_GUARDED_BY(sLock){0};
RefPtr<CacheHash> mRWHash MOZ_GUARDED_BY(sLock);
// True if read or write operation is pending. It is used to ensure that // mRWBuf is not freed until OnDataRead or OnDataWritten is called. bool mRWPending MOZ_GUARDED_BY(sLock){false};
// Reading of journal succeeded if true. bool mJournalReadSuccessfully MOZ_GUARDED_BY(sLock){false};
// Handle used for writing and reading index file.
RefPtr<CacheFileHandle> mIndexHandle MOZ_GUARDED_BY(sLock); // Handle used for reading journal file.
RefPtr<CacheFileHandle> mJournalHandle MOZ_GUARDED_BY(sLock); // Used to check the existence of the file during reading process.
RefPtr<CacheFileHandle> mTmpHandle MOZ_GUARDED_BY(sLock);
// Directory enumerator used when building and updating index.
nsCOMPtr<nsIDirectoryEnumerator> mDirEnumerator MOZ_GUARDED_BY(sLock);
// Main index hashtable.
nsTHashtable<CacheIndexEntry> mIndex MOZ_GUARDED_BY(sLock);
// We cannot add, remove or change any entry in mIndex in states READING and // WRITING. We track all changes in mPendingUpdates during these states.
nsTHashtable<CacheIndexEntryUpdate> mPendingUpdates MOZ_GUARDED_BY(sLock);
// Contains information statistics for mIndex + mPendingUpdates.
CacheIndexStats mIndexStats MOZ_GUARDED_BY(sLock);
// When reading journal, we must first parse the whole file and apply the // changes iff the journal was read successfully. mTmpJournal is used to store // entries from the journal file. We throw away all these entries if parsing // of the journal fails or the hash does not match.
nsTHashtable<CacheIndexEntry> mTmpJournal MOZ_GUARDED_BY(sLock);
// FrecencyArray maintains order of entry records for eviction. Ideally, the // records would be ordered by frecency all the time, but since this would be // quite expensive, we allow certain amount of entries to be out of order. // When the frecency is updated the new value is always bigger than the old // one. Instead of keeping updated entries at the same position, we move them // at the end of the array. This protects recently updated entries from // eviction. The array is sorted once we hit the limit of maximum unsorted // entries. class FrecencyArray { class Iterator { public: explicit Iterator(nsTArray<RefPtr<CacheIndexRecordWrapper>>* aRecs)
: mRecs(aRecs), mIdx(0) { while (!Done() && !(*mRecs)[mIdx]) {
mIdx++;
}
}
nsTArray<RefPtr<CacheIndexRecordWrapper>> mRecs;
uint32_t mUnsortedElements{0}; // Instead of removing elements from the array immediately, we null them out // and the iterator skips them when accessing the array. The null pointers // are placed at the end during sorting and we strip them out all at once. // This saves moving a lot of memory in nsTArray::RemoveElementsAt.
uint32_t mRemovedElements{0};
};
// This flag is true iff we are between CacheStorageService:Clear() and // processing all contexts to be evicted. It will make UI to show // "calculating" instead of any intermediate cache size. bool mAsyncGetDiskConsumptionBlocked MOZ_GUARDED_BY(sLock){false};
class DiskConsumptionObserver : public Runnable { public: static DiskConsumptionObserver* Init(
nsICacheStorageConsumptionObserver* aObserver) {
nsWeakPtr observer = do_GetWeakReference(aObserver); if (!observer) return nullptr;
if (observer) {
observer->OnNetworkCacheDiskConsumption(mSize);
}
return NS_OK;
}
nsWeakPtr mObserver;
int64_t mSize;
};
// List of async observers that want to get disk consumption information
nsTArray<RefPtr<DiskConsumptionObserver>> mDiskConsumptionObservers
MOZ_GUARDED_BY(sLock);
// Number of bytes written to the cache since the last telemetry report
uint64_t mTotalBytesWritten MOZ_GUARDED_BY(sLock){0};
};
} // namespace net
} // namespace mozilla
#endif
¤ Dauer der Verarbeitung: 0.30 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.