diff --git a/src/lib/caching/kshareddatacache.cpp b/src/lib/caching/kshareddatacache.cpp index 253d280..5b0b958 100644 --- a/src/lib/caching/kshareddatacache.cpp +++ b/src/lib/caching/kshareddatacache.cpp @@ -1,1717 +1,1717 @@ /* * This file is part of the KDE project. * Copyright © 2010, 2012 Michael Pyne * Copyright © 2012 Ralf Jung * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation. * * This library includes "MurmurHash" code from Austin Appleby, which is * placed in the public domain. See http://sites.google.com/site/murmurhash/ * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kshareddatacache.h" #include "kshareddatacache_p.h" // Various auxiliary support code #include "kcoreaddons_debug.h" #include "qstandardpaths.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /// The maximum number of probes to make while searching for a bucket in /// the presence of collisions in the cache index table. static const uint MAX_PROBE_COUNT = 6; /** * A very simple class whose only purpose is to be thrown as an exception from * underlying code to indicate that the shared cache is apparently corrupt. * This must be caught by top-level library code and used to unlink the cache * in this circumstance. * * @internal */ class KSDCCorrupted { public: KSDCCorrupted() { qCritical() << "Error detected in cache, re-generating"; } }; //----------------------------------------------------------------------------- // MurmurHashAligned, by Austin Appleby // (Released to the public domain, or licensed under the MIT license where // software may not be released to the public domain. See // http://sites.google.com/site/murmurhash/) // Same algorithm as MurmurHash, but only does aligned reads - should be safer // on certain platforms. static unsigned int MurmurHashAligned(const void *key, int len, unsigned int seed) { const unsigned int m = 0xc6a4a793; const int r = 16; const unsigned char *data = reinterpret_cast(key); unsigned int h = seed ^ (len * m); int align = reinterpret_cast(data) & 3; if (align && len >= 4) { // Pre-load the temp registers unsigned int t = 0, d = 0; switch (align) { case 1: t |= data[2] << 16; case 2: t |= data[1] << 8; case 3: t |= data[0]; } t <<= (8 * align); data += 4 - align; len -= 4 - align; int sl = 8 * (4 - align); int sr = 8 * align; // Mix while (len >= 4) { d = *reinterpret_cast(data); t = (t >> sr) | (d << sl); h += t; h *= m; h ^= h >> r; t = d; data += 4; len -= 4; } // Handle leftover data in temp registers int pack = len < align ? len : align; d = 0; switch (pack) { case 3: d |= data[2] << 16; case 2: d |= data[1] << 8; case 1: d |= data[0]; case 0: h += (t >> sr) | (d << sl); h *= m; h ^= h >> r; } data += pack; len -= pack; } else { while (len >= 4) { h += *reinterpret_cast(data); h *= m; h ^= h >> r; data += 4; len -= 4; } } //---------- // Handle tail bytes switch (len) { case 3: h += data[2] << 16; case 2: h += data[1] << 8; case 1: h += data[0]; h *= m; h ^= h >> r; }; h *= m; h ^= h >> 10; h *= m; h ^= h >> 17; return h; } /** * This is the hash function used for our data to hopefully make the * hashing used to place the QByteArrays as efficient as possible. */ static quint32 generateHash(const QByteArray &buffer) { // The final constant is the "seed" for MurmurHash. Do *not* change it // without incrementing the cache version. return MurmurHashAligned(buffer.data(), buffer.size(), 0xF0F00F0F); } // Alignment concerns become a big deal when we're dealing with shared memory, // since trying to access a structure sized at, say 8 bytes at an address that // is not evenly divisible by 8 is a crash-inducing error on some // architectures. The compiler would normally take care of this, but with // shared memory the compiler will not necessarily know the alignment expected, // so make sure we account for this ourselves. To do so we need a way to find // out the expected alignment. Enter ALIGNOF... #ifndef ALIGNOF #if defined(Q_CC_GNU) || defined(Q_CC_SUN) #define ALIGNOF(x) (__alignof__ (x)) // GCC provides what we want directly #else #include // offsetof template struct __alignmentHack { char firstEntry; T obj; static const size_t size = offsetof(__alignmentHack, obj); }; #define ALIGNOF(x) (__alignmentHack::size) #endif // Non gcc #endif // ALIGNOF undefined // Returns a pointer properly aligned to handle size alignment. // size should be a power of 2. start is assumed to be the lowest // permissible address, therefore the return value will be >= start. template T *alignTo(const void *start, uint size = ALIGNOF(T)) { quintptr mask = size - 1; // Cast to int-type to handle bit-twiddling quintptr basePointer = reinterpret_cast(start); // If (and only if) we are already aligned, adding mask into basePointer // will not increment any of the bits in ~mask and we get the right answer. basePointer = (basePointer + mask) & ~mask; return reinterpret_cast(basePointer); } /** * Returns a pointer to a const object of type T, assumed to be @p offset * *BYTES* greater than the base address. Note that in order to meet alignment * requirements for T, it is possible that the returned pointer points greater * than @p offset into @p base. */ template const T *offsetAs(const void *const base, qint32 offset) { const char *ptr = reinterpret_cast(base); return alignTo(ptr + offset); } // Same as above, but for non-const objects template T *offsetAs(void *const base, qint32 offset) { char *ptr = reinterpret_cast(base); return alignTo(ptr + offset); } /** * @return the smallest integer greater than or equal to (@p a / @p b). * @param a Numerator, should be ≥ 0. * @param b Denominator, should be > 0. */ static unsigned intCeil(unsigned a, unsigned b) { // The overflow check is unsigned and so is actually defined behavior. if (Q_UNLIKELY(b == 0 || ((a + b) < a))) { throw KSDCCorrupted(); } return (a + b - 1) / b; } /** * @return number of set bits in @p value (see also "Hamming weight") */ static unsigned countSetBits(unsigned value) { // K&R / Wegner's algorithm used. GCC supports __builtin_popcount but we // expect there to always be only 1 bit set so this should be perhaps a bit // faster 99.9% of the time. unsigned count = 0; for (count = 0; value != 0; count++) { value &= (value - 1); // Clears least-significant set bit. } return count; } typedef qint32 pageID; // ========================================================================= // Description of the cache: // // The shared memory cache is designed to be handled as two separate objects, // all contained in the same global memory segment. First off, there is the // basic header data, consisting of the global header followed by the // accounting data necessary to hold items (described in more detail // momentarily). Following the accounting data is the start of the "page table" // (essentially just as you'd see it in an Operating Systems text). // // The page table contains shared memory split into fixed-size pages, with a // configurable page size. In the event that the data is too large to fit into // a single logical page, it will need to occupy consecutive pages of memory. // // The accounting data that was referenced earlier is split into two: // // 1. index table, containing a fixed-size list of possible cache entries. // Each index entry is of type IndexTableEntry (below), and holds the various // accounting data and a pointer to the first page. // // 2. page table, which is used to speed up the process of searching for // free pages of memory. There is one entry for every page in the page table, // and it contains the index of the one entry in the index table actually // holding the page (or <0 if the page is free). // // The entire segment looks like so: // ?════════?═════════════?════════════?═══════?═══════?═══════?═══════?═══? // ? Header │ Index Table │ Page Table ? Pages │ │ │ │...? // ?════════?═════════════?════════════?═══════?═══════?═══════?═══════?═══? // ========================================================================= // All elements of this struct must be "plain old data" (POD) types since it // will be in shared memory. In addition, no pointers! To point to something // you must use relative offsets since the pointer start addresses will be // different in each process. struct IndexTableEntry { uint fileNameHash; uint totalItemSize; // in bytes mutable uint useCount; time_t addTime; mutable time_t lastUsedTime; pageID firstPage; }; // Page table entry struct PageTableEntry { // int so we can use values <0 for unassigned pages. qint32 index; }; // Each individual page contains the cached data. The first page starts off with // the utf8-encoded key, a null '\0', and then the data follows immediately // from the next byte, possibly crossing consecutive page boundaries to hold // all of the data. // There is, however, no specific struct for a page, it is simply a location in // memory. // This is effectively the layout of the shared memory segment. The variables // contained within form the header, data contained afterwards is pointed to // by using special accessor functions. struct SharedMemory { /** * Note to downstream packagers: This version flag is intended to be * machine-specific. The KDE-provided source code will not set the lower * two bits to allow for distribution-specific needs, with the exception * of version 1 which was already defined in KDE Platform 4.5. * e.g. the next version bump will be from 4 to 8, then 12, etc. */ enum { PIXMAP_CACHE_VERSION = 12, MINIMUM_CACHE_SIZE = 4096 }; // Note to those who follow me. You should not, under any circumstances, ever // re-arrange the following two fields, even if you change the version number // for later revisions of this code. QAtomicInt ready; ///< DO NOT INITIALIZE quint8 version; // See kshareddatacache_p.h SharedLock shmLock; uint cacheSize; uint cacheAvail; QAtomicInt evictionPolicy; // pageSize and cacheSize determine the number of pages. The number of // pages determine the page table size and (indirectly) the index table // size. QAtomicInt pageSize; // This variable is added to reserve space for later cache timestamping // support. The idea is this variable will be updated when the cache is // written to, to allow clients to detect a changed cache quickly. QAtomicInt cacheTimestamp; /** * Converts the given average item size into an appropriate page size. */ static unsigned equivalentPageSize(unsigned itemSize) { if (itemSize == 0) { return 4096; // Default average item size. } int log2OfSize = 0; while ((itemSize >>= 1) != 0) { log2OfSize++; } // Bound page size between 512 bytes and 256 KiB. // If this is adjusted, also alter validSizeMask in cachePageSize log2OfSize = qBound(9, log2OfSize, 18); return (1 << log2OfSize); } // Returns pageSize in unsigned format. unsigned cachePageSize() const { unsigned _pageSize = static_cast(pageSize.load()); // bits 9-18 may be set. static const unsigned validSizeMask = 0x7FE00u; // Check for page sizes that are not a power-of-2, or are too low/high. if (Q_UNLIKELY(countSetBits(_pageSize) != 1 || (_pageSize & ~validSizeMask))) { throw KSDCCorrupted(); } return _pageSize; } /** * This is effectively the class ctor. But since we're in shared memory, * there's a few rules: * * 1. To allow for some form of locking in the initial-setup case, we * use an atomic int, which will be initialized to 0 by mmap(). Then to * take the lock we atomically increment the 0 to 1. If we end up calling * the QAtomicInt constructor we can mess that up, so we can't use a * constructor for this class either. * 2. Any member variable you add takes up space in shared memory as well, * so make sure you need it. */ bool performInitialSetup(uint _cacheSize, uint _pageSize) { if (_cacheSize < MINIMUM_CACHE_SIZE) { qCritical() << "Internal error: Attempted to create a cache sized < " << MINIMUM_CACHE_SIZE; return false; } if (_pageSize == 0) { qCritical() << "Internal error: Attempted to create a cache with 0-sized pages."; return false; } shmLock.type = findBestSharedLock(); if (shmLock.type == LOCKTYPE_INVALID) { qCritical() << "Unable to find an appropriate lock to guard the shared cache. " << "This *should* be essentially impossible. :("; return false; } bool isProcessShared = false; QSharedPointer tempLock(createLockFromId(shmLock.type, shmLock)); if (!tempLock->initialize(isProcessShared)) { qCritical() << "Unable to initialize the lock for the cache!"; return false; } if (!isProcessShared) { qCWarning(KCOREADDONS_DEBUG) << "Cache initialized, but does not support being" << "shared across processes."; } // These must be updated to make some of our auxiliary functions // work right since their values will be based on the cache size. cacheSize = _cacheSize; pageSize = _pageSize; version = PIXMAP_CACHE_VERSION; cacheTimestamp = static_cast(::time(nullptr)); clearInternalTables(); // Unlock the mini-lock, and introduce a total memory barrier to make // sure all changes have propagated even without a mutex. ready.ref(); return true; } void clearInternalTables() { // Assumes we're already locked somehow. cacheAvail = pageTableSize(); // Setup page tables to point nowhere PageTableEntry *table = pageTable(); for (uint i = 0; i < pageTableSize(); ++i) { table[i].index = -1; } // Setup index tables to be accurate. IndexTableEntry *indices = indexTable(); for (uint i = 0; i < indexTableSize(); ++i) { indices[i].firstPage = -1; indices[i].useCount = 0; indices[i].fileNameHash = 0; indices[i].totalItemSize = 0; indices[i].addTime = 0; indices[i].lastUsedTime = 0; } } const IndexTableEntry *indexTable() const { // Index Table goes immediately after this struct, at the first byte // where alignment constraints are met (accounted for by offsetAs). return offsetAs(this, sizeof(*this)); } const PageTableEntry *pageTable() const { const IndexTableEntry *base = indexTable(); base += indexTableSize(); // Let's call wherever we end up the start of the page table... return alignTo(base); } const void *cachePages() const { const PageTableEntry *tableStart = pageTable(); tableStart += pageTableSize(); // Let's call wherever we end up the start of the data... return alignTo(tableStart, cachePageSize()); } const void *page(pageID at) const { if (static_cast(at) >= pageTableSize()) { return nullptr; } // We must manually calculate this one since pageSize varies. const char *pageStart = reinterpret_cast(cachePages()); pageStart += (at * cachePageSize()); return reinterpret_cast(pageStart); } // The following are non-const versions of some of the methods defined // above. They use const_cast<> because I feel that is better than // duplicating the code. I suppose template member functions (?) // may work, may investigate later. IndexTableEntry *indexTable() { const SharedMemory *that = const_cast(this); return const_cast(that->indexTable()); } PageTableEntry *pageTable() { const SharedMemory *that = const_cast(this); return const_cast(that->pageTable()); } void *cachePages() { const SharedMemory *that = const_cast(this); return const_cast(that->cachePages()); } void *page(pageID at) { const SharedMemory *that = const_cast(this); return const_cast(that->page(at)); } uint pageTableSize() const { return cacheSize / cachePageSize(); } uint indexTableSize() const { // Assume 2 pages on average are needed -> the number of entries // would be half of the number of pages. return pageTableSize() / 2; } /** * @return the index of the first page, for the set of contiguous * pages that can hold @p pagesNeeded PAGES. */ pageID findEmptyPages(uint pagesNeeded) const { if (Q_UNLIKELY(pagesNeeded > pageTableSize())) { return pageTableSize(); } // Loop through the page table, find the first empty page, and just // makes sure that there are enough free pages. const PageTableEntry *table = pageTable(); uint contiguousPagesFound = 0; pageID base = 0; for (pageID i = 0; i < static_cast(pageTableSize()); ++i) { if (table[i].index < 0) { if (contiguousPagesFound == 0) { base = i; } contiguousPagesFound++; } else { contiguousPagesFound = 0; } if (contiguousPagesFound == pagesNeeded) { return base; } } return pageTableSize(); } // left < right? static bool lruCompare(const IndexTableEntry &l, const IndexTableEntry &r) { // Ensure invalid entries migrate to the end if (l.firstPage < 0 && r.firstPage >= 0) { return false; } if (l.firstPage >= 0 && r.firstPage < 0) { return true; } // Most recently used will have the highest absolute time => // least recently used (lowest) should go first => use left < right return l.lastUsedTime < r.lastUsedTime; } // left < right? static bool seldomUsedCompare(const IndexTableEntry &l, const IndexTableEntry &r) { // Ensure invalid entries migrate to the end if (l.firstPage < 0 && r.firstPage >= 0) { return false; } if (l.firstPage >= 0 && r.firstPage < 0) { return true; } // Put lowest use count at start by using left < right return l.useCount < r.useCount; } // left < right? static bool ageCompare(const IndexTableEntry &l, const IndexTableEntry &r) { // Ensure invalid entries migrate to the end if (l.firstPage < 0 && r.firstPage >= 0) { return false; } if (l.firstPage >= 0 && r.firstPage < 0) { return true; } // Oldest entries die first -- they have the lowest absolute add time, // so just like the others use left < right return l.addTime < r.addTime; } void defragment() { if (cacheAvail * cachePageSize() == cacheSize) { return; // That was easy } qCDebug(KCOREADDONS_DEBUG) << "Defragmenting the shared cache"; // Just do a linear scan, and anytime there is free space, swap it // with the pages to its right. In order to meet the precondition // we need to skip any used pages first. pageID currentPage = 0; pageID idLimit = static_cast(pageTableSize()); PageTableEntry *pages = pageTable(); if (Q_UNLIKELY(!pages || idLimit <= 0)) { throw KSDCCorrupted(); } // Skip used pages while (currentPage < idLimit && pages[currentPage].index >= 0) { ++currentPage; } pageID freeSpot = currentPage; // Main loop, starting from a free page, skip to the used pages and // move them back. while (currentPage < idLimit) { // Find the next used page while (currentPage < idLimit && pages[currentPage].index < 0) { ++currentPage; } if (currentPage >= idLimit) { break; } // Found an entry, move it. qint32 affectedIndex = pages[currentPage].index; if (Q_UNLIKELY(affectedIndex < 0 || affectedIndex >= idLimit || indexTable()[affectedIndex].firstPage != currentPage)) { throw KSDCCorrupted(); } indexTable()[affectedIndex].firstPage = freeSpot; // Moving one page at a time guarantees we can use memcpy safely // (in other words, the source and destination will not overlap). while (currentPage < idLimit && pages[currentPage].index >= 0) { const void *const sourcePage = page(currentPage); void *const destinationPage = page(freeSpot); // We always move used pages into previously-found empty spots, // so check ordering as well for logic errors. if (Q_UNLIKELY(!sourcePage || !destinationPage || sourcePage < destinationPage)) { throw KSDCCorrupted(); } ::memcpy(destinationPage, sourcePage, cachePageSize()); pages[freeSpot].index = affectedIndex; pages[currentPage].index = -1; ++currentPage; ++freeSpot; // If we've just moved the very last page and it happened to // be at the very end of the cache then we're done. if (currentPage >= idLimit) { break; } // We're moving consecutive used pages whether they belong to // our affected entry or not, so detect if we've started moving // the data for a different entry and adjust if necessary. if (affectedIndex != pages[currentPage].index) { indexTable()[pages[currentPage].index].firstPage = freeSpot; } affectedIndex = pages[currentPage].index; } // At this point currentPage is on a page that is unused, and the // cycle repeats. However, currentPage is not the first unused // page, freeSpot is, so leave it alone. } } /** * Finds the index entry for a given key. * @param key UTF-8 encoded key to search for. * @return The index of the entry in the cache named by @p key. Returns * <0 if no such entry is present. */ qint32 findNamedEntry(const QByteArray &key) const { uint keyHash = generateHash(key); uint position = keyHash % indexTableSize(); uint probeNumber = 1; // See insert() for description // Imagine 3 entries A, B, C in this logical probing chain. If B is // later removed then we can't find C either. So, we must keep // searching for probeNumber number of tries (or we find the item, // obviously). while (indexTable()[position].fileNameHash != keyHash && probeNumber < MAX_PROBE_COUNT) { position = (keyHash + (probeNumber + probeNumber * probeNumber) / 2) % indexTableSize(); probeNumber++; } if (indexTable()[position].fileNameHash == keyHash) { pageID firstPage = indexTable()[position].firstPage; if (firstPage < 0 || static_cast(firstPage) >= pageTableSize()) { return -1; } const void *resultPage = page(firstPage); if (Q_UNLIKELY(!resultPage)) { throw KSDCCorrupted(); } const char *utf8FileName = reinterpret_cast(resultPage); if (qstrncmp(utf8FileName, key.constData(), cachePageSize()) == 0) { return position; } } return -1; // Not found, or a different one found. } // Function to use with QSharedPointer in removeUsedPages below... static void deleteTable(IndexTableEntry *table) { delete [] table; } /** * Removes the requested number of pages. * * @param numberNeeded the number of pages required to fulfill a current request. * This number should be <0 and <= the number of pages in the cache. * @return The identifier of the beginning of a consecutive block of pages able * to fill the request. Returns a value >= pageTableSize() if no such * request can be filled. * @internal */ uint removeUsedPages(uint numberNeeded) { if (numberNeeded == 0) { qCritical() << "Internal error: Asked to remove exactly 0 pages for some reason."; throw KSDCCorrupted(); } if (numberNeeded > pageTableSize()) { qCritical() << "Internal error: Requested more space than exists in the cache."; qCritical() << numberNeeded << "requested, " << pageTableSize() << "is the total possible."; throw KSDCCorrupted(); } // If the cache free space is large enough we will defragment first // instead since it's likely we're highly fragmented. // Otherwise, we will (eventually) simply remove entries per the // eviction order set for the cache until there is enough room // available to hold the number of pages we need. qCDebug(KCOREADDONS_DEBUG) << "Removing old entries to free up" << numberNeeded << "pages," << cacheAvail << "are already theoretically available."; if (cacheAvail > 3 * numberNeeded) { defragment(); uint result = findEmptyPages(numberNeeded); if (result < pageTableSize()) { return result; } else { qCritical() << "Just defragmented a locked cache, but still there" << "isn't enough room for the current request."; } } // At this point we know we'll have to free some space up, so sort our // list of entries by whatever the current criteria are and start // killing expired entries. QSharedPointer tablePtr(new IndexTableEntry[indexTableSize()], deleteTable); if (!tablePtr) { qCritical() << "Unable to allocate temporary memory for sorting the cache!"; clearInternalTables(); throw KSDCCorrupted(); } // We use tablePtr to ensure the data is destroyed, but do the access // via a helper pointer to allow for array ops. IndexTableEntry *table = tablePtr.data(); ::memcpy(table, indexTable(), sizeof(IndexTableEntry) * indexTableSize()); // Our entry ID is simply its index into the // index table, which qSort will rearrange all willy-nilly, so first // we'll save the *real* entry ID into firstPage (which is useless in // our copy of the index table). On the other hand if the entry is not // used then we note that with -1. for (uint i = 0; i < indexTableSize(); ++i) { table[i].firstPage = table[i].useCount > 0 ? static_cast(i) : -1; } // Declare the comparison function that we'll use to pass to qSort, // based on our cache eviction policy. bool (*compareFunction)(const IndexTableEntry &, const IndexTableEntry &); switch (evictionPolicy.load()) { case KSharedDataCache::EvictLeastOftenUsed: case KSharedDataCache::NoEvictionPreference: default: compareFunction = seldomUsedCompare; break; case KSharedDataCache::EvictLeastRecentlyUsed: compareFunction = lruCompare; break; case KSharedDataCache::EvictOldest: compareFunction = ageCompare; break; } qSort(table, table + indexTableSize(), compareFunction); // Least recently used entries will be in the front. // Start killing until we have room. // Note on removeEntry: It expects an index into the index table, // but our sorted list is all jumbled. But we stored the real index // in the firstPage member. // Remove entries until we've removed at least the required number // of pages. uint i = 0; while (i < indexTableSize() && numberNeeded > cacheAvail) { int curIndex = table[i++].firstPage; // Really an index, not a page // Removed everything, still no luck (or curIndex is set but too high). if (curIndex < 0 || static_cast(curIndex) >= indexTableSize()) { qCritical() << "Trying to remove index" << curIndex << "out-of-bounds for index table of size" << indexTableSize(); throw KSDCCorrupted(); } qCDebug(KCOREADDONS_DEBUG) << "Removing entry of" << indexTable()[curIndex].totalItemSize << "size"; removeEntry(curIndex); } // At this point let's see if we have freed up enough data by // defragmenting first and seeing if we can find that free space. defragment(); pageID result = pageTableSize(); while (i < indexTableSize() && (static_cast(result = findEmptyPages(numberNeeded))) >= pageTableSize()) { int curIndex = table[i++].firstPage; if (curIndex < 0) { // One last shot. defragment(); return findEmptyPages(numberNeeded); } if (Q_UNLIKELY(static_cast(curIndex) >= indexTableSize())) { throw KSDCCorrupted(); } removeEntry(curIndex); } // Whew. return result; } // Returns the total size required for a given cache size. static uint totalSize(uint cacheSize, uint effectivePageSize) { uint numberPages = intCeil(cacheSize, effectivePageSize); uint indexTableSize = numberPages / 2; // Knowing the number of pages, we can determine what addresses we'd be // using (properly aligned), and from there determine how much memory // we'd use. IndexTableEntry *indexTableStart = offsetAs(static_cast(nullptr), sizeof(SharedMemory)); indexTableStart += indexTableSize; PageTableEntry *pageTableStart = reinterpret_cast(indexTableStart); pageTableStart = alignTo(pageTableStart); pageTableStart += numberPages; // The weird part, we must manually adjust the pointer based on the page size. char *cacheStart = reinterpret_cast(pageTableStart); cacheStart += (numberPages * effectivePageSize); // ALIGNOF gives pointer alignment cacheStart = alignTo(cacheStart, ALIGNOF(void *)); // We've traversed the header, index, page table, and cache. // Wherever we're at now is the size of the enchilada. return static_cast(reinterpret_cast(cacheStart)); } uint fileNameHash(const QByteArray &utf8FileName) const { return generateHash(utf8FileName) % indexTableSize(); } void clear() { clearInternalTables(); } void removeEntry(uint index); }; // The per-instance private data, such as map size, whether // attached or not, pointer to shared memory, etc. -class KSharedDataCache::Private +class Q_DECL_HIDDEN KSharedDataCache::Private { public: Private(const QString &name, unsigned defaultCacheSize, unsigned expectedItemSize ) : m_cacheName(name) , shm(nullptr) , m_lock() , m_mapSize(0) , m_defaultCacheSize(defaultCacheSize) , m_expectedItemSize(expectedItemSize) , m_expectedType(LOCKTYPE_INVALID) { mapSharedMemory(); } // Put the cache in a condition to be able to call mapSharedMemory() by // completely detaching from shared memory (such as to respond to an // unrecoverable error). // m_mapSize must already be set to the amount of memory mapped to shm. void detachFromSharedMemory() { // The lock holds a reference into shared memory, so this must be // cleared before shm is removed. m_lock.clear(); if (shm && 0 != ::munmap(shm, m_mapSize)) { qCritical() << "Unable to unmap shared memory segment" << static_cast(shm) << ":" << ::strerror(errno); } shm = nullptr; m_mapSize = 0; } // This function does a lot of the important work, attempting to connect to shared // memory, a private anonymous mapping if that fails, and failing that, nothing (but // the cache remains "valid", we just don't actually do anything). void mapSharedMemory() { // 0-sized caches are fairly useless. unsigned cacheSize = qMax(m_defaultCacheSize, uint(SharedMemory::MINIMUM_CACHE_SIZE)); unsigned pageSize = SharedMemory::equivalentPageSize(m_expectedItemSize); // Ensure that the cache is sized such that there is a minimum number of // pages available. (i.e. a cache consisting of only 1 page is fairly // useless and probably crash-prone). cacheSize = qMax(pageSize * 256, cacheSize); // The m_cacheName is used to find the file to store the cache in. const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); QString cacheName = cacheDir + QLatin1String("/") + m_cacheName + QLatin1String(".kcache"); QFile file(cacheName); QFileInfo fileInfo(file); if (!QDir().mkpath(fileInfo.absolutePath())) { return; } // The basic idea is to open the file that we want to map into shared // memory, and then actually establish the mapping. Once we have mapped the // file into shared memory we can close the file handle, the mapping will // still be maintained (unless the file is resized to be shorter than // expected, which we don't handle yet :-( ) // size accounts for the overhead over the desired cacheSize uint size = SharedMemory::totalSize(cacheSize, pageSize); void *mapAddress = MAP_FAILED; if (size < cacheSize) { qCritical() << "Asked for a cache size less than requested size somehow -- Logic Error :("; return; } // We establish the shared memory mapping here, only if we will have appropriate // mutex support (systemSupportsProcessSharing), then we: // Open the file and resize to some sane value if the file is too small. if (file.open(QIODevice::ReadWrite) && (file.size() >= size || (file.resize(size) && ensureFileAllocated(file.handle(), size)))) { // Use mmap directly instead of QFile::map since the QFile (and its // shared mapping) will disappear unless we hang onto the QFile for no // reason (see the note below, we don't care about the file per se...) mapAddress = QT_MMAP(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, file.handle(), 0); // So... it is possible that someone else has mapped this cache already // with a larger size. If that's the case we need to at least match // the size to be able to access every entry, so fixup the mapping. if (mapAddress != MAP_FAILED) { SharedMemory *mapped = reinterpret_cast(mapAddress); // First make sure that the version of the cache on disk is // valid. We also need to check that version != 0 to // disambiguate against an uninitialized cache. if (mapped->version != SharedMemory::PIXMAP_CACHE_VERSION && mapped->version > 0) { qCWarning(KCOREADDONS_DEBUG) << "Deleting wrong version of cache" << cacheName; // CAUTION: Potentially recursive since the recovery // involves calling this function again. m_mapSize = size; shm = mapped; recoverCorruptedCache(); return; } else if (mapped->cacheSize > cacheSize) { // This order is very important. We must save the cache size // before we remove the mapping, but unmap before overwriting // the previous mapping size... cacheSize = mapped->cacheSize; unsigned actualPageSize = mapped->cachePageSize(); ::munmap(mapAddress, size); size = SharedMemory::totalSize(cacheSize, actualPageSize); mapAddress = QT_MMAP(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, file.handle(), 0); } } } // We could be here without the mapping established if: // 1) Process-shared synchronization is not supported, either at compile or run time, // 2) Unable to open the required file. // 3) Unable to resize the file to be large enough. // 4) Establishing the mapping failed. // 5) The mapping succeeded, but the size was wrong and we were unable to map when // we tried again. // 6) The incorrect version of the cache was detected. // 7) The file could be created, but posix_fallocate failed to commit it fully to disk. // In any of these cases, attempt to fallback to the // better-supported anonymous private page style of mmap. This memory won't // be shared, but our code will still work the same. // NOTE: We never use the on-disk representation independently of the // shared memory. If we don't get shared memory the disk info is ignored, // if we do get shared memory we never look at disk again. if (mapAddress == MAP_FAILED) { qCWarning(KCOREADDONS_DEBUG) << "Failed to establish shared memory mapping, will fallback" << "to private memory -- memory usage will increase"; mapAddress = QT_MMAP(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); } // Well now we're really hosed. We can still work, but we can't even cache // data. if (mapAddress == MAP_FAILED) { qCritical() << "Unable to allocate shared memory segment for shared data cache" << cacheName << "of size" << cacheSize; return; } m_mapSize = size; // We never actually construct shm, but we assign it the same address as the // shared memory we just mapped, so effectively shm is now a SharedMemory that // happens to be located at mapAddress. shm = reinterpret_cast(mapAddress); // If we were first to create this memory map, all data will be 0. // Therefore if ready == 0 we're not initialized. A fully initialized // header will have ready == 2. Why? // Because 0 means "safe to initialize" // 1 means "in progress of initing" // 2 means "ready" uint usecSleepTime = 8; // Start by sleeping for 8 microseconds while (shm->ready.load() != 2) { if (Q_UNLIKELY(usecSleepTime >= (1 << 21))) { // Didn't acquire within ~8 seconds? Assume an issue exists qCritical() << "Unable to acquire shared lock, is the cache corrupt?"; file.remove(); // Unlink the cache in case it's corrupt. detachFromSharedMemory(); return; // Fallback to QCache (later) } if (shm->ready.testAndSetAcquire(0, 1)) { if (!shm->performInitialSetup(cacheSize, pageSize)) { qCritical() << "Unable to perform initial setup, this system probably " "does not really support process-shared pthreads or " "semaphores, even though it claims otherwise."; file.remove(); detachFromSharedMemory(); return; } } else { usleep(usecSleepTime); // spin // Exponential fallback as in Ethernet and similar collision resolution methods usecSleepTime *= 2; } } m_expectedType = shm->shmLock.type; m_lock = QSharedPointer(createLockFromId(m_expectedType, shm->shmLock)); bool isProcessSharingSupported = false; if (!m_lock->initialize(isProcessSharingSupported)) { qCritical() << "Unable to setup shared cache lock, although it worked when created."; detachFromSharedMemory(); } } // Called whenever the cache is apparently corrupt (for instance, a timeout trying to // lock the cache). In this situation it is safer just to destroy it all and try again. void recoverCorruptedCache() { KSharedDataCache::deleteCache(m_cacheName); detachFromSharedMemory(); // Do this even if we weren't previously cached -- it might work now. mapSharedMemory(); } // This should be called for any memory access to shared memory. This // function will verify that the bytes [base, base+accessLength) are // actually mapped to d->shm. The cache itself may have incorrect cache // page sizes, incorrect cache size, etc. so this function should be called // despite the cache data indicating it should be safe. // // If the access is /not/ safe then a KSDCCorrupted exception will be // thrown, so be ready to catch that. void verifyProposedMemoryAccess(const void *base, unsigned accessLength) const { quintptr startOfAccess = reinterpret_cast(base); quintptr startOfShm = reinterpret_cast(shm); if (Q_UNLIKELY(startOfAccess < startOfShm)) { throw KSDCCorrupted(); } quintptr endOfShm = startOfShm + m_mapSize; quintptr endOfAccess = startOfAccess + accessLength; // Check for unsigned integer wraparound, and then // bounds access if (Q_UNLIKELY((endOfShm < startOfShm) || (endOfAccess < startOfAccess) || (endOfAccess > endOfShm))) { throw KSDCCorrupted(); } } bool lock() const { if (Q_LIKELY(shm && shm->shmLock.type == m_expectedType)) { return m_lock->lock(); } // No shm or wrong type --> corrupt! throw KSDCCorrupted(); } void unlock() const { m_lock->unlock(); } class CacheLocker { mutable Private *d; bool cautiousLock() { int lockCount = 0; // Locking can fail due to a timeout. If it happens too often even though // we're taking corrective action assume there's some disastrous problem // and give up. while (!d->lock() && !isLockedCacheSafe()) { d->recoverCorruptedCache(); if (!d->shm) { qCWarning(KCOREADDONS_DEBUG) << "Lost the connection to shared memory for cache" << d->m_cacheName; return false; } if (lockCount++ > 4) { qCritical() << "There is a very serious problem with the KDE data cache" << d->m_cacheName << "giving up trying to access cache."; d->detachFromSharedMemory(); return false; } } return true; } // Runs a quick battery of tests on an already-locked cache and returns // false as soon as a sanity check fails. The cache remains locked in this // situation. bool isLockedCacheSafe() const { // Note that cachePageSize() itself runs a check that can throw. uint testSize = SharedMemory::totalSize(d->shm->cacheSize, d->shm->cachePageSize()); if (Q_UNLIKELY(d->m_mapSize != testSize)) { return false; } if (Q_UNLIKELY(d->shm->version != SharedMemory::PIXMAP_CACHE_VERSION)) { return false; } switch (d->shm->evictionPolicy.load()) { case NoEvictionPreference: // fallthrough case EvictLeastRecentlyUsed: // fallthrough case EvictLeastOftenUsed: // fallthrough case EvictOldest: break; default: return false; } return true; } public: CacheLocker(const Private *_d) : d(const_cast(_d)) { if (Q_UNLIKELY(!d || !d->shm || !cautiousLock())) { d = nullptr; } } ~CacheLocker() { if (d && d->shm) { d->unlock(); } } bool failed() const { return !d || d->shm == nullptr; } }; QString m_cacheName; SharedMemory *shm; QSharedPointer m_lock; uint m_mapSize; uint m_defaultCacheSize; uint m_expectedItemSize; SharedLockId m_expectedType; }; // Must be called while the lock is already held! void SharedMemory::removeEntry(uint index) { if (index >= indexTableSize() || cacheAvail > pageTableSize()) { throw KSDCCorrupted(); } PageTableEntry *pageTableEntries = pageTable(); IndexTableEntry *entriesIndex = indexTable(); // Update page table first pageID firstPage = entriesIndex[index].firstPage; if (firstPage < 0 || static_cast(firstPage) >= pageTableSize()) { qCDebug(KCOREADDONS_DEBUG) << "Trying to remove an entry which is already invalid. This " << "cache is likely corrupt."; throw KSDCCorrupted(); } if (index != static_cast(pageTableEntries[firstPage].index)) { qCritical() << "Removing entry" << index << "but the matching data" << "doesn't link back -- cache is corrupt, clearing."; throw KSDCCorrupted(); } uint entriesToRemove = intCeil(entriesIndex[index].totalItemSize, cachePageSize()); uint savedCacheSize = cacheAvail; for (uint i = firstPage; i < pageTableSize() && static_cast(pageTableEntries[i].index) == index; ++i) { pageTableEntries[i].index = -1; cacheAvail++; } if ((cacheAvail - savedCacheSize) != entriesToRemove) { qCritical() << "We somehow did not remove" << entriesToRemove << "when removing entry" << index << ", instead we removed" << (cacheAvail - savedCacheSize); throw KSDCCorrupted(); } // For debugging #ifdef NDEBUG void *const startOfData = page(firstPage); if (startOfData) { QByteArray str((const char *) startOfData); str.prepend(" REMOVED: "); str.prepend(QByteArray::number(index)); str.prepend("ENTRY "); ::memcpy(startOfData, str.constData(), str.size() + 1); } #endif // Update the index entriesIndex[index].fileNameHash = 0; entriesIndex[index].totalItemSize = 0; entriesIndex[index].useCount = 0; entriesIndex[index].lastUsedTime = 0; entriesIndex[index].addTime = 0; entriesIndex[index].firstPage = -1; } KSharedDataCache::KSharedDataCache(const QString &cacheName, unsigned defaultCacheSize, unsigned expectedItemSize) : d(nullptr) { try { d = new Private(cacheName, defaultCacheSize, expectedItemSize); } catch (KSDCCorrupted) { KSharedDataCache::deleteCache(cacheName); // Try only once more try { d = new Private(cacheName, defaultCacheSize, expectedItemSize); } catch (KSDCCorrupted) { qCritical() << "Even a brand-new cache starts off corrupted, something is" << "seriously wrong. :-("; d = nullptr; // Just in case } } } KSharedDataCache::~KSharedDataCache() { // Note that there is no other actions required to separate from the // shared memory segment, simply unmapping is enough. This makes things // *much* easier so I'd recommend maintaining this ideal. if (!d) { return; } if (d->shm) { #ifdef KSDC_MSYNC_SUPPORTED ::msync(d->shm, d->m_mapSize, MS_INVALIDATE | MS_ASYNC); #endif ::munmap(d->shm, d->m_mapSize); } // Do not delete d->shm, it was never constructed, it's just an alias. d->shm = nullptr; delete d; } bool KSharedDataCache::insert(const QString &key, const QByteArray &data) { try { Private::CacheLocker lock(d); if (lock.failed()) { return false; } QByteArray encodedKey = key.toUtf8(); uint keyHash = generateHash(encodedKey); uint position = keyHash % d->shm->indexTableSize(); // See if we're overwriting an existing entry. IndexTableEntry *indices = d->shm->indexTable(); // In order to avoid the issue of a very long-lived cache having items // with a use count of 1 near-permanently, we attempt to artifically // reduce the use count of long-lived items when there is high load on // the cache. We do this randomly, with a weighting that makes the event // impossible if load < 0.5, and guaranteed if load >= 0.96. const static double startCullPoint = 0.5l; const static double mustCullPoint = 0.96l; // cacheAvail is in pages, cacheSize is in bytes. double loadFactor = 1.0 - (1.0l * d->shm->cacheAvail * d->shm->cachePageSize() / d->shm->cacheSize); bool cullCollisions = false; if (Q_UNLIKELY(loadFactor >= mustCullPoint)) { cullCollisions = true; } else if (loadFactor > startCullPoint) { const int tripWireValue = RAND_MAX * (loadFactor - startCullPoint) / (mustCullPoint - startCullPoint); if (KRandom::random() >= tripWireValue) { cullCollisions = true; } } // In case of collisions in the index table (i.e. identical positions), use // quadratic chaining to attempt to find an empty slot. The equation we use // is: // position = (hash + (i + i*i) / 2) % size, where i is the probe number. uint probeNumber = 1; while (indices[position].useCount > 0 && probeNumber < MAX_PROBE_COUNT) { // If we actually stumbled upon an old version of the key we are // overwriting, then use that position, do not skip over it. if (Q_UNLIKELY(indices[position].fileNameHash == keyHash)) { break; } // If we are "culling" old entries, see if this one is old and if so // reduce its use count. If it reduces to zero then eliminate it and // use its old spot. if (cullCollisions && (::time(nullptr) - indices[position].lastUsedTime) > 60) { indices[position].useCount >>= 1; if (indices[position].useCount == 0) { qCDebug(KCOREADDONS_DEBUG) << "Overwriting existing old cached entry due to collision."; d->shm->removeEntry(position); // Remove it first break; } } position = (keyHash + (probeNumber + probeNumber * probeNumber) / 2) % d->shm->indexTableSize(); probeNumber++; } if (indices[position].useCount > 0 && indices[position].firstPage >= 0) { //qCDebug(KCOREADDONS_DEBUG) << "Overwriting existing cached entry due to collision."; d->shm->removeEntry(position); // Remove it first } // Data will be stored as fileNamefoo\0PNGimagedata..... // So total size required is the length of the encoded file name + 1 // for the trailing null, and then the length of the image data. uint fileNameLength = 1 + encodedKey.length(); uint requiredSize = fileNameLength + data.size(); uint pagesNeeded = intCeil(requiredSize, d->shm->cachePageSize()); uint firstPage(-1); if (pagesNeeded >= d->shm->pageTableSize()) { qCWarning(KCOREADDONS_DEBUG) << key << "is too large to be cached."; return false; } // If the cache has no room, or the fragmentation is too great to find // the required number of consecutive free pages, take action. if (pagesNeeded > d->shm->cacheAvail || (firstPage = d->shm->findEmptyPages(pagesNeeded)) >= d->shm->pageTableSize()) { // If we have enough free space just defragment uint freePagesDesired = 3 * qMax(1u, pagesNeeded / 2); if (d->shm->cacheAvail > freePagesDesired) { // TODO: How the hell long does this actually take on real // caches? d->shm->defragment(); firstPage = d->shm->findEmptyPages(pagesNeeded); } else { // If we already have free pages we don't want to remove a ton // extra. However we can't rely on the return value of // removeUsedPages giving us a good location since we're not // passing in the actual number of pages that we need. d->shm->removeUsedPages(qMin(2 * freePagesDesired, d->shm->pageTableSize()) - d->shm->cacheAvail); firstPage = d->shm->findEmptyPages(pagesNeeded); } if (firstPage >= d->shm->pageTableSize() || d->shm->cacheAvail < pagesNeeded) { qCritical() << "Unable to free up memory for" << key; return false; } } // Update page table PageTableEntry *table = d->shm->pageTable(); for (uint i = 0; i < pagesNeeded; ++i) { table[firstPage + i].index = position; } // Update index indices[position].fileNameHash = keyHash; indices[position].totalItemSize = requiredSize; indices[position].useCount = 1; indices[position].addTime = ::time(nullptr); indices[position].lastUsedTime = indices[position].addTime; indices[position].firstPage = firstPage; // Update cache d->shm->cacheAvail -= pagesNeeded; // Actually move the data in place void *dataPage = d->shm->page(firstPage); if (Q_UNLIKELY(!dataPage)) { throw KSDCCorrupted(); } // Verify it will all fit d->verifyProposedMemoryAccess(dataPage, requiredSize); // Cast for byte-sized pointer arithmetic uchar *startOfPageData = reinterpret_cast(dataPage); ::memcpy(startOfPageData, encodedKey.constData(), fileNameLength); ::memcpy(startOfPageData + fileNameLength, data.constData(), data.size()); return true; } catch (KSDCCorrupted) { d->recoverCorruptedCache(); return false; } } bool KSharedDataCache::find(const QString &key, QByteArray *destination) const { try { Private::CacheLocker lock(d); if (lock.failed()) { return false; } // Search in the index for our data, hashed by key; QByteArray encodedKey = key.toUtf8(); qint32 entry = d->shm->findNamedEntry(encodedKey); if (entry >= 0) { const IndexTableEntry *header = &d->shm->indexTable()[entry]; const void *resultPage = d->shm->page(header->firstPage); if (Q_UNLIKELY(!resultPage)) { throw KSDCCorrupted(); } d->verifyProposedMemoryAccess(resultPage, header->totalItemSize); header->useCount++; header->lastUsedTime = ::time(nullptr); // Our item is the key followed immediately by the data, so skip // past the key. const char *cacheData = reinterpret_cast(resultPage); cacheData += encodedKey.size(); cacheData++; // Skip trailing null -- now we're pointing to start of data if (destination) { *destination = QByteArray(cacheData, header->totalItemSize - encodedKey.size() - 1); } return true; } } catch (KSDCCorrupted) { d->recoverCorruptedCache(); } return false; } void KSharedDataCache::clear() { try { Private::CacheLocker lock(d); if (!lock.failed()) { d->shm->clear(); } } catch (KSDCCorrupted) { d->recoverCorruptedCache(); } } bool KSharedDataCache::contains(const QString &key) const { try { Private::CacheLocker lock(d); if (lock.failed()) { return false; } return d->shm->findNamedEntry(key.toUtf8()) >= 0; } catch (KSDCCorrupted) { d->recoverCorruptedCache(); return false; } } void KSharedDataCache::deleteCache(const QString &cacheName) { QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/") + cacheName + QLatin1String(".kcache"); // Note that it is important to simply unlink the file, and not truncate it // smaller first to avoid SIGBUS errors and similar with shared memory // attached to the underlying inode. qCDebug(KCOREADDONS_DEBUG) << "Removing cache at" << cachePath; QFile::remove(cachePath); } unsigned KSharedDataCache::totalSize() const { try { Private::CacheLocker lock(d); if (lock.failed()) { return 0u; } return d->shm->cacheSize; } catch (KSDCCorrupted) { d->recoverCorruptedCache(); return 0u; } } unsigned KSharedDataCache::freeSize() const { try { Private::CacheLocker lock(d); if (lock.failed()) { return 0u; } return d->shm->cacheAvail * d->shm->cachePageSize(); } catch (KSDCCorrupted) { d->recoverCorruptedCache(); return 0u; } } KSharedDataCache::EvictionPolicy KSharedDataCache::evictionPolicy() const { if (d && d->shm) { return static_cast(d->shm->evictionPolicy.fetchAndAddAcquire(0)); } return NoEvictionPreference; } void KSharedDataCache::setEvictionPolicy(EvictionPolicy newPolicy) { if (d && d->shm) { d->shm->evictionPolicy.fetchAndStoreRelease(static_cast(newPolicy)); } } unsigned KSharedDataCache::timestamp() const { if (d && d->shm) { return static_cast(d->shm->cacheTimestamp.fetchAndAddAcquire(0)); } return 0; } void KSharedDataCache::setTimestamp(unsigned newTimestamp) { if (d && d->shm) { d->shm->cacheTimestamp.fetchAndStoreRelease(static_cast(newTimestamp)); } } diff --git a/src/lib/caching/kshareddatacache_win.cpp b/src/lib/caching/kshareddatacache_win.cpp index ecfa6b9..872cd2d 100644 --- a/src/lib/caching/kshareddatacache_win.cpp +++ b/src/lib/caching/kshareddatacache_win.cpp @@ -1,119 +1,119 @@ /* * This file is part of the KDE project. * Copyright 2010 Michael Pyne * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ /** * This is a horrifically simple implementation of KSharedDataCache that is * basically missing the "shared" part to it, for use on Windows or other platforms * that don't support POSIX. */ #include "kshareddatacache.h" #include #include #include -class KSharedDataCache::Private +class Q_DECL_HIDDEN KSharedDataCache::Private { public: KSharedDataCache::EvictionPolicy evictionPolicy; QCache cache; }; KSharedDataCache::KSharedDataCache(const QString &cacheName, unsigned defaultCacheSize, unsigned expectedItemSize) : d(new Private) { d->cache.setMaxCost(defaultCacheSize); Q_UNUSED(cacheName); Q_UNUSED(expectedItemSize); } KSharedDataCache::~KSharedDataCache() { delete d; } KSharedDataCache::EvictionPolicy KSharedDataCache::evictionPolicy() const { return d->evictionPolicy; } void KSharedDataCache::setEvictionPolicy(KSharedDataCache::EvictionPolicy newPolicy) { d->evictionPolicy = newPolicy; } bool KSharedDataCache::insert(const QString &key, const QByteArray &data) { return d->cache.insert(key, new QByteArray(data)); } bool KSharedDataCache::find(const QString &key, QByteArray *destination) const { QByteArray *value = d->cache.object(key); if (value) { if (destination) { *destination = *value; } return true; } else { return false; } } void KSharedDataCache::clear() { d->cache.clear(); } void KSharedDataCache::deleteCache(const QString &cacheName) { Q_UNUSED(cacheName); } bool KSharedDataCache::contains(const QString &key) const { return d->cache.contains(key); } unsigned KSharedDataCache::totalSize() const { return static_cast(d->cache.maxCost()); } unsigned KSharedDataCache::freeSize() const { if (d->cache.totalCost() < d->cache.maxCost()) { return static_cast(d->cache.maxCost() - d->cache.totalCost()); } else { return 0; } } unsigned KSharedDataCache::timestamp() const { return 0; } void KSharedDataCache::setTimestamp(unsigned newTimestamp) { } diff --git a/src/lib/jobs/kjobtrackerinterface.cpp b/src/lib/jobs/kjobtrackerinterface.cpp index 11e132d..f25ee65 100644 --- a/src/lib/jobs/kjobtrackerinterface.cpp +++ b/src/lib/jobs/kjobtrackerinterface.cpp @@ -1,150 +1,150 @@ /* This file is part of the KDE project Copyright (C) 2007 Kevin Ottens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kjobtrackerinterface.h" #include "kjob.h" -class KJobTrackerInterface::Private +class Q_DECL_HIDDEN KJobTrackerInterface::Private { public: Private(KJobTrackerInterface *interface) : q(interface) { } KJobTrackerInterface *const q; }; KJobTrackerInterface::KJobTrackerInterface(QObject *parent) : QObject(parent), d(new Private(this)) { qRegisterMetaType>(); } KJobTrackerInterface::~KJobTrackerInterface() { delete d; } void KJobTrackerInterface::registerJob(KJob *job) { QObject::connect(job, SIGNAL(finished(KJob*)), this, SLOT(unregisterJob(KJob*))); QObject::connect(job, SIGNAL(finished(KJob*)), this, SLOT(finished(KJob*))); QObject::connect(job, SIGNAL(suspended(KJob*)), this, SLOT(suspended(KJob*))); QObject::connect(job, SIGNAL(resumed(KJob*)), this, SLOT(resumed(KJob*))); QObject::connect(job, SIGNAL(description(KJob *, const QString &, const QPair &, const QPair &)), this, SLOT(description(KJob *, const QString &, const QPair &, const QPair &))); QObject::connect(job, SIGNAL(infoMessage(KJob*,QString,QString)), this, SLOT(infoMessage(KJob*,QString,QString))); QObject::connect(job, SIGNAL(warning(KJob*,QString,QString)), this, SLOT(warning(KJob*,QString,QString))); QObject::connect(job, SIGNAL(totalAmount(KJob*,KJob::Unit,qulonglong)), this, SLOT(totalAmount(KJob*,KJob::Unit,qulonglong))); QObject::connect(job, SIGNAL(processedAmount(KJob*,KJob::Unit,qulonglong)), this, SLOT(processedAmount(KJob*,KJob::Unit,qulonglong))); QObject::connect(job, SIGNAL(percent(KJob*,ulong)), this, SLOT(percent(KJob*,ulong))); QObject::connect(job, SIGNAL(speed(KJob*,ulong)), this, SLOT(speed(KJob*,ulong))); } void KJobTrackerInterface::unregisterJob(KJob *job) { job->disconnect(this); } void KJobTrackerInterface::finished(KJob *job) { Q_UNUSED(job) } void KJobTrackerInterface::suspended(KJob *job) { Q_UNUSED(job) } void KJobTrackerInterface::resumed(KJob *job) { Q_UNUSED(job) } void KJobTrackerInterface::description(KJob *job, const QString &title, const QPair &field1, const QPair &field2) { Q_UNUSED(job) Q_UNUSED(title) Q_UNUSED(field1) Q_UNUSED(field2) } void KJobTrackerInterface::infoMessage(KJob *job, const QString &plain, const QString &rich) { Q_UNUSED(job) Q_UNUSED(plain) Q_UNUSED(rich) } void KJobTrackerInterface::warning(KJob *job, const QString &plain, const QString &rich) { Q_UNUSED(job) Q_UNUSED(plain) Q_UNUSED(rich) } void KJobTrackerInterface::totalAmount(KJob *job, KJob::Unit unit, qulonglong amount) { Q_UNUSED(job) Q_UNUSED(unit) Q_UNUSED(amount) } void KJobTrackerInterface::processedAmount(KJob *job, KJob::Unit unit, qulonglong amount) { Q_UNUSED(job) Q_UNUSED(unit) Q_UNUSED(amount) } void KJobTrackerInterface::percent(KJob *job, unsigned long percent) { Q_UNUSED(job) Q_UNUSED(percent) } void KJobTrackerInterface::speed(KJob *job, unsigned long value) { Q_UNUSED(job) Q_UNUSED(value) } #include "moc_kjobtrackerinterface.cpp" diff --git a/src/lib/jobs/kjobuidelegate.cpp b/src/lib/jobs/kjobuidelegate.cpp index c9d29b9..7ee0f5f 100644 --- a/src/lib/jobs/kjobuidelegate.cpp +++ b/src/lib/jobs/kjobuidelegate.cpp @@ -1,120 +1,120 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow David Faure Copyright (C) 2006 Kevin Ottens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kjobuidelegate.h" #include "kjob.h" -class KJobUiDelegate::Private +class Q_DECL_HIDDEN KJobUiDelegate::Private { public: Private(KJobUiDelegate *delegate) : q(delegate), job(nullptr), autoErrorHandling(false), autoWarningHandling(true) { } KJobUiDelegate *const q; KJob *job; bool autoErrorHandling : 1; bool autoWarningHandling : 1; void connectJob(KJob *job); void _k_result(KJob *job); }; KJobUiDelegate::KJobUiDelegate() : QObject(), d(new Private(this)) { } KJobUiDelegate::~KJobUiDelegate() { delete d; } bool KJobUiDelegate::setJob(KJob *job) { if (d->job != nullptr) { return false; } d->job = job; setParent(job); return true; } KJob *KJobUiDelegate::job() const { return d->job; } void KJobUiDelegate::showErrorMessage() { } void KJobUiDelegate::setAutoErrorHandlingEnabled(bool enable) { d->autoErrorHandling = enable; } bool KJobUiDelegate::isAutoErrorHandlingEnabled() const { return d->autoErrorHandling; } void KJobUiDelegate::setAutoWarningHandlingEnabled(bool enable) { d->autoWarningHandling = enable; } bool KJobUiDelegate::isAutoWarningHandlingEnabled() const { return d->autoWarningHandling; } void KJobUiDelegate::slotWarning(KJob *job, const QString &plain, const QString &rich) { Q_UNUSED(job) Q_UNUSED(plain) Q_UNUSED(rich) } void KJobUiDelegate::connectJob(KJob *job) { connect(job, SIGNAL(result(KJob*)), this, SLOT(_k_result(KJob*))); connect(job, SIGNAL(warning(KJob*,QString,QString)), this, SLOT(slotWarning(KJob*,QString,QString))); } void KJobUiDelegate::Private::_k_result(KJob *job2) { Q_UNUSED(job2) if (job->error() && autoErrorHandling) { q->showErrorMessage(); } } #include "moc_kjobuidelegate.cpp" diff --git a/src/lib/kaboutdata.cpp b/src/lib/kaboutdata.cpp index 1333eae..237ddef 100644 --- a/src/lib/kaboutdata.cpp +++ b/src/lib/kaboutdata.cpp @@ -1,1193 +1,1193 @@ /* * This file is part of the KDE Libraries * Copyright (C) 2000 Espen Sand (espen@kde.org) * Copyright (C) 2006 Nicolas GOUTTE * Copyright (C) 2008 Friedrich W. H. Kossebau * Copyright (C) 2010 Teo Mrnjavac * Copyright (C) 2017 Harald Sitter * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "kaboutdata.h" #include "kpluginmetadata.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(KABOUTDATA) // logging category for this framework, default: log stuff >= warning Q_LOGGING_CATEGORY(KABOUTDATA, "kf5.kcoreaddons.kaboutdata", QtWarningMsg) -class KAboutPerson::Private +class Q_DECL_HIDDEN KAboutPerson::Private { public: QString _name; QString _task; QString _emailAddress; QString _webAddress; QString _ocsUsername; }; KAboutPerson::KAboutPerson(const QString &_name, const QString &_task, const QString &_emailAddress, const QString &_webAddress, const QString &_ocsUsername) : d(new Private) { d->_name = _name; d->_task = _task; d->_emailAddress = _emailAddress; d->_webAddress = _webAddress; d->_ocsUsername = _ocsUsername; } KAboutPerson::KAboutPerson(const QString &_name, const QString &_email, bool) : d(new Private) { d->_name = _name; d->_emailAddress = _email; } KAboutPerson::KAboutPerson(const KAboutPerson &other): d(new Private) { *d = *other.d; } KAboutPerson::~KAboutPerson() { delete d; } QString KAboutPerson::name() const { return d->_name; } QString KAboutPerson::task() const { return d->_task; } QString KAboutPerson::emailAddress() const { return d->_emailAddress; } QString KAboutPerson::webAddress() const { return d->_webAddress; } QString KAboutPerson::ocsUsername() const { return d->_ocsUsername; } KAboutPerson &KAboutPerson::operator=(const KAboutPerson &other) { *d = *other.d; return *this; } KAboutPerson KAboutPerson::fromJSON(const QJsonObject &obj) { const QString name = KPluginMetaData::readTranslatedString(obj, QStringLiteral("Name")); const QString task = KPluginMetaData::readTranslatedString(obj, QStringLiteral("Task")); const QString email = obj[QStringLiteral("Email")].toString(); const QString website = obj[QStringLiteral("Website")].toString(); const QString userName = obj[QStringLiteral("UserName")].toString(); return KAboutPerson(name, task, email, website, userName); } -class KAboutLicense::Private : public QSharedData +class Q_DECL_HIDDEN KAboutLicense::Private : public QSharedData { public: Private(LicenseKey licenseType, VersionRestriction versionRestriction, const KAboutData *aboutData); Private(const Private &other); QString spdxID() const; LicenseKey _licenseKey; QString _licenseText; QString _pathToLicenseTextFile; VersionRestriction _versionRestriction; // needed for access to the possibly changing copyrightStatement() const KAboutData *_aboutData; }; KAboutLicense::Private::Private(LicenseKey licenseType, VersionRestriction versionRestriction, const KAboutData *aboutData) : QSharedData(), _licenseKey(licenseType), _versionRestriction(versionRestriction), _aboutData(aboutData) { } KAboutLicense::Private::Private(const KAboutLicense::Private &other) : QSharedData(other), _licenseKey(other._licenseKey), _licenseText(other._licenseText), _pathToLicenseTextFile(other._pathToLicenseTextFile), _versionRestriction(other._versionRestriction), _aboutData(other._aboutData) {} QString KAboutLicense::Private::spdxID() const { switch (_licenseKey) { case KAboutLicense::GPL_V2: return QStringLiteral("GPL-2.0"); case KAboutLicense::LGPL_V2: return QStringLiteral("LGPL-2.0"); case KAboutLicense::BSDL: return QStringLiteral("BSD-2-Clause"); case KAboutLicense::Artistic: return QStringLiteral("Artistic-1.0"); case KAboutLicense::QPL_V1_0: return QStringLiteral("QPL-1.0"); case KAboutLicense::GPL_V3: return QStringLiteral("GPL-3.0"); case KAboutLicense::LGPL_V3: return QStringLiteral("LGPL-3.0"); case KAboutLicense::LGPL_V2_1: return QStringLiteral("LGPL-2.1"); case KAboutLicense::Custom: case KAboutLicense::File: case KAboutLicense::Unknown: return QString(); } return QString(); } KAboutLicense::KAboutLicense(LicenseKey licenseType, VersionRestriction versionRestriction, const KAboutData *aboutData) : d(new Private(licenseType, versionRestriction, aboutData)) { } KAboutLicense::KAboutLicense(LicenseKey licenseType, const KAboutData *aboutData) : d(new Private(licenseType, OnlyThisVersion, aboutData)) { } KAboutLicense::KAboutLicense(const KAboutData *aboutData) : d(new Private(Unknown, OnlyThisVersion, aboutData)) { } KAboutLicense::KAboutLicense(const KAboutLicense &other) : d(other.d) { } KAboutLicense::~KAboutLicense() {} void KAboutLicense::setLicenseFromPath(const QString &pathToFile) { d->_licenseKey = KAboutLicense::File; d->_pathToLicenseTextFile = pathToFile; } void KAboutLicense::setLicenseFromText(const QString &licenseText) { d->_licenseKey = KAboutLicense::Custom; d->_licenseText = licenseText; } QString KAboutLicense::text() const { QString result; const QString lineFeed = QStringLiteral("\n\n"); if (d->_aboutData && !d->_aboutData->copyrightStatement().isEmpty()) { result = d->_aboutData->copyrightStatement() + lineFeed; } bool knownLicense = false; QString pathToFile; // rel path if known license switch (d->_licenseKey) { case KAboutLicense::File: pathToFile = d->_pathToLicenseTextFile; break; case KAboutLicense::GPL_V2: knownLicense = true; pathToFile = QStringLiteral("GPL_V2"); break; case KAboutLicense::LGPL_V2: knownLicense = true; pathToFile = QStringLiteral("LGPL_V2"); break; case KAboutLicense::BSDL: knownLicense = true; pathToFile = QStringLiteral("BSD"); break; case KAboutLicense::Artistic: knownLicense = true; pathToFile = QStringLiteral("ARTISTIC"); break; case KAboutLicense::QPL_V1_0: knownLicense = true; pathToFile = QStringLiteral("QPL_V1.0"); break; case KAboutLicense::GPL_V3: knownLicense = true; pathToFile = QStringLiteral("GPL_V3"); break; case KAboutLicense::LGPL_V3: knownLicense = true; pathToFile = QStringLiteral("LGPL_V3"); break; case KAboutLicense::LGPL_V2_1: knownLicense = true; pathToFile = QStringLiteral("LGPL_V21"); break; case KAboutLicense::Custom: if (!d->_licenseText.isEmpty()) { result = d->_licenseText; break; } // fall through default: result += QCoreApplication::translate( "KAboutLicense", "No licensing terms for this program have been specified.\n" "Please check the documentation or the source for any\n" "licensing terms.\n"); } if (knownLicense) { pathToFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString::fromLatin1("kf5/licenses/") + pathToFile); result += QCoreApplication::translate( "KAboutLicense", "This program is distributed under the terms of the %1.").arg(name(KAboutLicense::ShortName)); if (!pathToFile.isEmpty()) { result += lineFeed; } } if (!pathToFile.isEmpty()) { QFile file(pathToFile); if (file.open(QIODevice::ReadOnly)) { QTextStream str(&file); result += str.readAll(); } } return result; } QString KAboutLicense::spdx() const { // SPDX licenses are comprised of an identifier (e.g. GPL-2.0), an optional + to denote 'or // later versions' and optional ' WITH $exception' to denote standardized exceptions from the // core license. As we do not offer exceptions we effectively only return GPL-2.0 or GPL-2.0+, // this may change in the future. To that end the documentation makes no assertations about the // actual content of the SPDX license expression we return. // Expressions can in theory also contain AND, OR and () to build constructs involving more than // one license. As this is outside the scope of a single license object we'll ignore this here // for now. // The expecation is that the return value is only run through spec-compliant parsers, so this // can potentially be changed. auto id = d->spdxID(); if (id.isNull()) { // Guard against potential future changes which would allow 'Foo+' as input. return id; } return d->_versionRestriction == OrLaterVersions ? id.append(QLatin1Char('+')) : id; } QString KAboutLicense::name(KAboutLicense::NameFormat formatName) const { QString licenseShort; QString licenseFull; switch (d->_licenseKey) { case KAboutLicense::GPL_V2: licenseShort = QCoreApplication::translate("KAboutLicense", "GPL v2", "@item license (short name)"); licenseFull = QCoreApplication::translate("KAboutLicense", "GNU General Public License Version 2", "@item license"); break; case KAboutLicense::LGPL_V2: licenseShort = QCoreApplication::translate("KAboutLicense", "LGPL v2", "@item license (short name)"); licenseFull = QCoreApplication::translate("KAboutLicense", "GNU Lesser General Public License Version 2", "@item license"); break; case KAboutLicense::BSDL: licenseShort = QCoreApplication::translate("KAboutLicense", "BSD License", "@item license (short name)"); licenseFull = QCoreApplication::translate("KAboutLicense", "BSD License", "@item license"); break; case KAboutLicense::Artistic: licenseShort = QCoreApplication::translate("KAboutLicense", "Artistic License", "@item license (short name)"); licenseFull = QCoreApplication::translate("KAboutLicense", "Artistic License", "@item license"); break; case KAboutLicense::QPL_V1_0: licenseShort = QCoreApplication::translate("KAboutLicense", "QPL v1.0", "@item license (short name)"); licenseFull = QCoreApplication::translate("KAboutLicense", "Q Public License", "@item license"); break; case KAboutLicense::GPL_V3: licenseShort = QCoreApplication::translate("KAboutLicense", "GPL v3", "@item license (short name)"); licenseFull = QCoreApplication::translate("KAboutLicense", "GNU General Public License Version 3", "@item license"); break; case KAboutLicense::LGPL_V3: licenseShort = QCoreApplication::translate("KAboutLicense", "LGPL v3", "@item license (short name)"); licenseFull = QCoreApplication::translate("KAboutLicense", "GNU Lesser General Public License Version 3", "@item license"); break; case KAboutLicense::LGPL_V2_1: licenseShort = QCoreApplication::translate("KAboutLicense", "LGPL v2.1", "@item license (short name)"); licenseFull = QCoreApplication::translate("KAboutLicense", "GNU Lesser General Public License Version 2.1", "@item license"); break; case KAboutLicense::Custom: case KAboutLicense::File: licenseShort = licenseFull = QCoreApplication::translate("KAboutLicense", "Custom", "@item license"); break; default: licenseShort = licenseFull = QCoreApplication::translate("KAboutLicense", "Not specified", "@item license"); } const QString result = (formatName == KAboutLicense::ShortName) ? licenseShort : (formatName == KAboutLicense::FullName) ? licenseFull : QString(); return result; } KAboutLicense &KAboutLicense::operator=(const KAboutLicense &other) { d = other.d; return *this; } KAboutLicense::LicenseKey KAboutLicense::key() const { return d->_licenseKey; } KAboutLicense KAboutLicense::byKeyword(const QString &rawKeyword) { // Setup keyword->enum dictionary on first call. // Use normalized keywords, by the algorithm below. static const QHash licenseDict { { "gpl", KAboutLicense::GPL }, { "gplv2", KAboutLicense::GPL_V2 }, { "gplv2+", KAboutLicense::GPL_V2 }, { "lgpl", KAboutLicense::LGPL }, { "lgplv2", KAboutLicense::LGPL_V2 }, { "lgplv2+", KAboutLicense::LGPL_V2 }, { "bsd", KAboutLicense::BSDL }, { "artistic", KAboutLicense::Artistic }, { "qpl", KAboutLicense::QPL }, { "qplv1", KAboutLicense::QPL_V1_0 }, { "qplv10", KAboutLicense::QPL_V1_0 }, { "gplv3", KAboutLicense::GPL_V3 }, { "gplv3+", KAboutLicense::GPL_V3 }, { "lgplv3", KAboutLicense::LGPL_V3 }, { "lgplv3+", KAboutLicense::LGPL_V3 }, { "lgplv21", KAboutLicense::LGPL_V2_1 }, { "lgplv21+", KAboutLicense::LGPL_V2_1 } }; // Normalize keyword. QString keyword = rawKeyword; keyword = keyword.toLower(); keyword.remove(QLatin1Char(' ')); keyword.remove(QLatin1Char('.')); LicenseKey license = licenseDict.value(keyword.toLatin1(), KAboutLicense::Custom); auto restriction = keyword.endsWith(QLatin1Char('+')) ? OrLaterVersions : OnlyThisVersion; return KAboutLicense(license, restriction, nullptr); } -class KAboutData::Private +class Q_DECL_HIDDEN KAboutData::Private { public: Private() : customAuthorTextEnabled(false) {} QString _componentName; QString _displayName; QString _shortDescription; QString _copyrightStatement; QString _otherText; QString _homepageAddress; QList _authorList; QList _creditList; QList _translatorList; QList _licenseList; QString productName; QString programIconName; QVariant programLogo; QString customAuthorPlainText, customAuthorRichText; bool customAuthorTextEnabled; QString organizationDomain; QString _ocsProviderUrl; QString desktopFileName; // Everything dr.konqi needs, we store as utf-8, so we // can just give it a pointer, w/o any allocations. QByteArray _internalProgramName; QByteArray _version; QByteArray _bugAddress; static QList parseTranslators(const QString &translatorName, const QString &translatorEmail); }; KAboutData::KAboutData(const QString &_componentName, const QString &_displayName, const QString &_version, const QString &_shortDescription, enum KAboutLicense::LicenseKey licenseType, const QString &_copyrightStatement, const QString &text, const QString &homePageAddress, const QString &bugAddress ) : d(new Private) { d->_componentName = _componentName; int p = d->_componentName.indexOf(QLatin1Char('/')); if (p >= 0) { d->_componentName = d->_componentName.mid(p + 1); } d->_displayName = _displayName; if (!d->_displayName.isEmpty()) { // KComponentData("klauncher") gives empty program name d->_internalProgramName = _displayName.toUtf8(); } d->_version = _version.toUtf8(); d->_shortDescription = _shortDescription; d->_licenseList.append(KAboutLicense(licenseType, this)); d->_copyrightStatement = _copyrightStatement; d->_otherText = text; d->_homepageAddress = homePageAddress; d->_bugAddress = bugAddress.toUtf8(); QUrl homePageUrl(homePageAddress); if (!homePageUrl.isValid() || homePageUrl.scheme().isEmpty()) { // Default domain if nothing else is better homePageUrl.setUrl(QStringLiteral("https://kde.org/")); } const QChar dotChar(QLatin1Char('.')); QStringList hostComponents = homePageUrl.host().split(dotChar); // Remove leading component unless 2 (or less) components are present if (hostComponents.size() > 2) { hostComponents.removeFirst(); } d->organizationDomain = hostComponents.join(dotChar); // KF6: do not set a default desktopFileName value here, but remove this code and leave it empty // see KAboutData::desktopFileName() for detals // desktop file name is reverse domain name std::reverse(hostComponents.begin(), hostComponents.end()); hostComponents.append(_componentName); d->desktopFileName = hostComponents.join(dotChar); } KAboutData::KAboutData(const QString &_componentName, const QString &_displayName, const QString &_version ) : d(new Private) { d->_componentName = _componentName; int p = d->_componentName.indexOf(QLatin1Char('/')); if (p >= 0) { d->_componentName = d->_componentName.mid(p + 1); } d->_displayName = _displayName; if (!d->_displayName.isEmpty()) { // KComponentData("klauncher") gives empty program name d->_internalProgramName = _displayName.toUtf8(); } d->_version = _version.toUtf8(); // match behaviour of other constructors d->_licenseList.append(KAboutLicense(KAboutLicense::Unknown, this)); d->_bugAddress = "submit@bugs.kde.org"; d->organizationDomain = QStringLiteral("kde.org"); // KF6: do not set a default desktopFileName value here, but remove this code and leave it empty // see KAboutData::desktopFileName() for detals d->desktopFileName = QStringLiteral("org.kde.%1").arg(d->_componentName); } KAboutData::~KAboutData() { delete d; } KAboutData::KAboutData(const KAboutData &other): d(new Private) { *d = *other.d; QList::iterator it = d->_licenseList.begin(), itEnd = d->_licenseList.end(); for (; it != itEnd; ++it) { KAboutLicense &al = *it; al.d.detach(); al.d->_aboutData = this; } } KAboutData &KAboutData::operator=(const KAboutData &other) { if (this != &other) { *d = *other.d; QList::iterator it = d->_licenseList.begin(), itEnd = d->_licenseList.end(); for (; it != itEnd; ++it) { KAboutLicense &al = *it; al.d.detach(); al.d->_aboutData = this; } } return *this; } KAboutData KAboutData::fromPluginMetaData(const KPluginMetaData &plugin) { KAboutData ret(plugin.pluginId(), plugin.name(), plugin.version(), plugin.description(), KAboutLicense::byKeyword(plugin.license()).key(), plugin.copyrightText(), plugin.extraInformation(), plugin.website()); ret.d->programIconName = plugin.iconName(); ret.d->_authorList = plugin.authors(); ret.d->_translatorList = plugin.translators(); ret.d->_creditList = plugin.otherContributors(); return ret; } KAboutData &KAboutData::addAuthor(const QString &name, const QString &task, const QString &emailAddress, const QString &webAddress, const QString &ocsUsername) { d->_authorList.append(KAboutPerson(name, task, emailAddress, webAddress, ocsUsername)); return *this; } KAboutData &KAboutData::addCredit(const QString &name, const QString &task, const QString &emailAddress, const QString &webAddress, const QString &ocsUsername) { d->_creditList.append(KAboutPerson(name, task, emailAddress, webAddress, ocsUsername)); return *this; } KAboutData &KAboutData::setTranslator(const QString &name, const QString &emailAddress) { d->_translatorList = Private::parseTranslators(name, emailAddress); return *this; } KAboutData &KAboutData::setLicenseText(const QString &licenseText) { d->_licenseList[0] = KAboutLicense(this); d->_licenseList[0].setLicenseFromText(licenseText); return *this; } KAboutData &KAboutData::addLicenseText(const QString &licenseText) { // if the default license is unknown, overwrite instead of append KAboutLicense &firstLicense = d->_licenseList[0]; KAboutLicense newLicense(this); newLicense.setLicenseFromText(licenseText); if (d->_licenseList.count() == 1 && firstLicense.d->_licenseKey == KAboutLicense::Unknown) { firstLicense = newLicense; } else { d->_licenseList.append(newLicense); } return *this; } KAboutData &KAboutData::setLicenseTextFile(const QString &pathToFile) { d->_licenseList[0] = KAboutLicense(this); d->_licenseList[0].setLicenseFromPath(pathToFile); return *this; } KAboutData &KAboutData::addLicenseTextFile(const QString &pathToFile) { // if the default license is unknown, overwrite instead of append KAboutLicense &firstLicense = d->_licenseList[0]; KAboutLicense newLicense(this); newLicense.setLicenseFromPath(pathToFile); if (d->_licenseList.count() == 1 && firstLicense.d->_licenseKey == KAboutLicense::Unknown) { firstLicense = newLicense; } else { d->_licenseList.append(newLicense); } return *this; } KAboutData &KAboutData::setComponentName(const QString &componentName) { d->_componentName = componentName; return *this; } KAboutData &KAboutData::setDisplayName(const QString &_displayName) { d->_displayName = _displayName; d->_internalProgramName = _displayName.toUtf8(); return *this; } KAboutData &KAboutData::setOcsProvider(const QString &_ocsProviderUrl) { d->_ocsProviderUrl = _ocsProviderUrl; return *this; } KAboutData &KAboutData::setVersion(const QByteArray &_version) { d->_version = _version; return *this; } KAboutData &KAboutData::setShortDescription(const QString &_shortDescription) { d->_shortDescription = _shortDescription; return *this; } KAboutData &KAboutData::setLicense(KAboutLicense::LicenseKey licenseKey) { return setLicense(licenseKey, KAboutLicense::OnlyThisVersion); } KAboutData &KAboutData::setLicense(KAboutLicense::LicenseKey licenseKey, KAboutLicense::VersionRestriction versionRestriction) { d->_licenseList[0] = KAboutLicense(licenseKey, versionRestriction, this); return *this; } KAboutData &KAboutData::addLicense(KAboutLicense::LicenseKey licenseKey) { return addLicense(licenseKey, KAboutLicense::OnlyThisVersion); } KAboutData &KAboutData::addLicense(KAboutLicense::LicenseKey licenseKey, KAboutLicense::VersionRestriction versionRestriction) { // if the default license is unknown, overwrite instead of append KAboutLicense &firstLicense = d->_licenseList[0]; if (d->_licenseList.count() == 1 && firstLicense.d->_licenseKey == KAboutLicense::Unknown) { firstLicense = KAboutLicense(licenseKey, versionRestriction, this); } else { d->_licenseList.append(KAboutLicense(licenseKey, versionRestriction, this)); } return *this; } KAboutData &KAboutData::setCopyrightStatement(const QString &_copyrightStatement) { d->_copyrightStatement = _copyrightStatement; return *this; } KAboutData &KAboutData::setOtherText(const QString &_otherText) { d->_otherText = _otherText; return *this; } KAboutData &KAboutData::setHomepage(const QString &homepage) { d->_homepageAddress = homepage; return *this; } KAboutData &KAboutData::setBugAddress(const QByteArray &_bugAddress) { d->_bugAddress = _bugAddress; return *this; } KAboutData &KAboutData::setOrganizationDomain(const QByteArray &domain) { d->organizationDomain = QString::fromLatin1(domain.data()); return *this; } KAboutData &KAboutData::setProductName(const QByteArray &_productName) { d->productName = QString::fromUtf8(_productName.data()); return *this; } QString KAboutData::componentName() const { return d->_componentName; } QString KAboutData::productName() const { if (!d->productName.isEmpty()) { return d->productName; } return componentName(); } QString KAboutData::displayName() const { if (!d->_displayName.isEmpty()) { return d->_displayName; } return componentName(); } /// @internal /// Return the program name. It is always pre-allocated. /// Needed for KCrash in particular. const char *KAboutData::internalProgramName() const { return d->_internalProgramName.constData(); } QString KAboutData::programIconName() const { return d->programIconName.isEmpty() ? componentName() : d->programIconName; } KAboutData &KAboutData::setProgramIconName(const QString &iconName) { d->programIconName = iconName; return *this; } QVariant KAboutData::programLogo() const { return d->programLogo; } KAboutData &KAboutData::setProgramLogo(const QVariant &image) { d->programLogo = image; return *this; } QString KAboutData::ocsProviderUrl() const { return d->_ocsProviderUrl; } QString KAboutData::version() const { return QString::fromUtf8(d->_version.data()); } /// @internal /// Return the untranslated and uninterpreted (to UTF8) string /// for the version information. Used in particular for KCrash. const char *KAboutData::internalVersion() const { return d->_version.constData(); } QString KAboutData::shortDescription() const { return d->_shortDescription; } QString KAboutData::homepage() const { return d->_homepageAddress; } QString KAboutData::bugAddress() const { return QString::fromUtf8(d->_bugAddress.constData()); } QString KAboutData::organizationDomain() const { return d->organizationDomain; } /// @internal /// Return the untranslated and uninterpreted (to UTF8) string /// for the bug mail address. Used in particular for KCrash. const char *KAboutData::internalBugAddress() const { if (d->_bugAddress.isEmpty()) { return nullptr; } return d->_bugAddress.constData(); } QList KAboutData::authors() const { return d->_authorList; } QList KAboutData::credits() const { return d->_creditList; } QList KAboutData::Private::parseTranslators(const QString &translatorName, const QString &translatorEmail) { QList personList; if (translatorName.isEmpty() || translatorName == QStringLiteral("Your names")) { return personList; } const QStringList nameList(translatorName.split(QLatin1Char(','))); QStringList emailList; if (!translatorEmail.isEmpty() && translatorEmail != QStringLiteral("Your emails")) { emailList = translatorEmail.split(QLatin1Char(','), QString::KeepEmptyParts); } QStringList::const_iterator nit; QStringList::const_iterator eit = emailList.constBegin(); for (nit = nameList.constBegin(); nit != nameList.constEnd(); ++nit) { QString email; if (eit != emailList.constEnd()) { email = *eit; ++eit; } personList.append(KAboutPerson((*nit).trimmed(), email.trimmed(), true)); } return personList; } QList KAboutData::translators() const { return d->_translatorList; } QString KAboutData::aboutTranslationTeam() { return QCoreApplication::translate( "KAboutData", "

KDE is translated into many languages thanks to the work " "of the translation teams all over the world.

" "

For more information on KDE internationalization " "visit http://l10n.kde.org

", "replace this with information about your translation team" ); } QString KAboutData::otherText() const { return d->_otherText; } QList KAboutData::licenses() const { return d->_licenseList; } QString KAboutData::copyrightStatement() const { return d->_copyrightStatement; } QString KAboutData::customAuthorPlainText() const { return d->customAuthorPlainText; } QString KAboutData::customAuthorRichText() const { return d->customAuthorRichText; } bool KAboutData::customAuthorTextEnabled() const { return d->customAuthorTextEnabled; } KAboutData &KAboutData::setCustomAuthorText(const QString &plainText, const QString &richText) { d->customAuthorPlainText = plainText; d->customAuthorRichText = richText; d->customAuthorTextEnabled = true; return *this; } KAboutData &KAboutData::unsetCustomAuthorText() { d->customAuthorPlainText = QString(); d->customAuthorRichText = QString(); d->customAuthorTextEnabled = false; return *this; } KAboutData &KAboutData::setDesktopFileName(const QString &desktopFileName) { d->desktopFileName = desktopFileName; return *this; } QString KAboutData::desktopFileName() const { return d->desktopFileName; // KF6: switch to this code and adapt API dox #if 0 // if desktopFileName has been explicitely set, use that value if (!d->desktopFileName.isEmpty()) { return d->desktopFileName; } // return a string calculated on-the-fly from the current org domain & component name const QChar dotChar(QLatin1Char('.')); QStringList hostComponents = d->organizationDomain.split(dotChar); // desktop file name is reverse domain name std::reverse(hostComponents.begin(), hostComponents.end()); hostComponents.append(componentName()); return hostComponents.join(dotChar); #endif } class KAboutDataRegistry { public: KAboutDataRegistry() : m_appData(nullptr) {} ~KAboutDataRegistry() { delete m_appData; qDeleteAll(m_pluginData); } KAboutData *m_appData; QHash m_pluginData; }; Q_GLOBAL_STATIC(KAboutDataRegistry, s_registry) namespace { void warnIfOutOfSync(const char *aboutDataString, const QString &aboutDataValue, const char *appDataString, const QString &appDataValue) { if (aboutDataValue != appDataValue) { qCWarning(KABOUTDATA) << appDataString <m_appData; // not yet existing if (!aboutData) { // init from current Q*Application data aboutData = new KAboutData(QCoreApplication::applicationName(), QString(), QString()); // For applicationDisplayName & desktopFileName, which are only properties of QGuiApplication, // we have to try to get them via the property system, as the static getter methods are // part of QtGui only. Disadvantage: requires an app instance. // Either get all or none of the properties & warn about it if (app) { aboutData->setOrganizationDomain(QCoreApplication::organizationDomain().toUtf8()); aboutData->setVersion(QCoreApplication::applicationVersion().toUtf8()); aboutData->setDisplayName(app->property("applicationDisplayName").toString()); aboutData->setDesktopFileName(app->property("desktopFileName").toString()); } else { qCWarning(KABOUTDATA) << "Could not initialize the properties of KAboutData::applicationData by the equivalent properties from Q*Application: no app instance (yet) existing."; } s_registry->m_appData = aboutData; } else { // check if in-sync with Q*Application metadata, as their setters could have been called // after the last KAboutData::setApplicationData, with different values warnIfOutOfSync("KAboutData::applicationData().componentName", aboutData->componentName(), "QCoreApplication::applicationName", QCoreApplication::applicationName()); warnIfOutOfSync("KAboutData::applicationData().version", aboutData->version(), "QCoreApplication::applicationVersion", QCoreApplication::applicationVersion()); warnIfOutOfSync("KAboutData::applicationData().organizationDomain", aboutData->organizationDomain(), "QCoreApplication::organizationDomain", QCoreApplication::organizationDomain()); if (app) { warnIfOutOfSync("KAboutData::applicationData().displayName", aboutData->displayName(), "QGuiApplication::applicationDisplayName", app->property("applicationDisplayName").toString()); warnIfOutOfSync("KAboutData::applicationData().desktopFileName", aboutData->desktopFileName(), "QGuiApplication::desktopFileName", app->property("desktopFileName").toString()); } } return *aboutData; } void KAboutData::setApplicationData(const KAboutData &aboutData) { if (s_registry->m_appData) { *s_registry->m_appData = aboutData; } else { s_registry->m_appData = new KAboutData(aboutData); } // For applicationDisplayName & desktopFileName, which are only properties of QGuiApplication, // we have to try to set them via the property system, as the static getter methods are // part of QtGui only. Disadvantage: requires an app instance. // So set either all or none of the properties & warn about it QCoreApplication *app = QCoreApplication::instance(); if (app) { app->setApplicationVersion(aboutData.version()); app->setApplicationName(aboutData.componentName()); app->setOrganizationDomain(aboutData.organizationDomain()); app->setProperty("applicationDisplayName", aboutData.displayName()); app->setProperty("desktopFileName", aboutData.desktopFileName()); } else { qCWarning(KABOUTDATA) << "Could not initialize the equivalent properties of Q*Application: no instance (yet) existing."; } // KF6: Rethink the current relation between KAboutData::applicationData and the Q*Application metadata // Always overwriting the Q*Application metadata here, but not updating back the KAboutData // in applicationData() is unbalanced and can result in out-of-sync data if the Q*Application // setters have been called meanwhile // Options are to remove the overlapping properties of KAboutData for cleancode, or making the // overlapping properties official shadow properties of their Q*Application countparts, though // that increases behavioural complexity a little. } void KAboutData::registerPluginData(const KAboutData &aboutData) { s_registry->m_pluginData.insert(aboutData.componentName(), new KAboutData(aboutData)); } KAboutData *KAboutData::pluginData(const QString &componentName) { KAboutData *ad = s_registry->m_pluginData.value(componentName); return ad; } // only for KCrash (no memory allocation allowed) const KAboutData *KAboutData::applicationDataPointer() { if (s_registry.exists()) { return s_registry->m_appData; } return nullptr; } bool KAboutData::setupCommandLine(QCommandLineParser *parser) { if (!d->_shortDescription.isEmpty()) { parser->setApplicationDescription(d->_shortDescription); } parser->addHelpOption(); QCoreApplication *app = QCoreApplication::instance(); if (app && !app->applicationVersion().isEmpty()) { parser->addVersionOption(); } return parser->addOption(QCommandLineOption(QStringLiteral("author"), QCoreApplication::translate("KAboutData CLI", "Show author information."))) && parser->addOption(QCommandLineOption(QStringLiteral("license"), QCoreApplication::translate("KAboutData CLI", "Show license information."))) && parser->addOption(QCommandLineOption(QStringLiteral("desktopfile"), QCoreApplication::translate("KAboutData CLI", "The base file name of the desktop entry for this application."), QCoreApplication::translate("KAboutData CLI", "file name"))); } void KAboutData::processCommandLine(QCommandLineParser *parser) { bool foundArgument = false; if (parser->isSet(QStringLiteral("author"))) { foundArgument = true; if (d->_authorList.isEmpty()) { printf("%s\n", qPrintable(QCoreApplication::translate("KAboutData CLI", "This application was written by somebody who wants to remain anonymous."))); } else { printf("%s\n", qPrintable(QCoreApplication::translate("KAboutData CLI", "%1 was written by:").arg(qAppName()))); Q_FOREACH (const KAboutPerson &person, d->_authorList) { QString authorData = QStringLiteral(" ") + person.name(); if (!person.emailAddress().isEmpty()) { authorData.append(QStringLiteral(" <") + person.emailAddress() + QStringLiteral(">")); } printf("%s\n", qPrintable(authorData));; } } if (!customAuthorTextEnabled()) { if (bugAddress() == QLatin1String("submit@bugs.kde.org") ) { printf("%s\n", qPrintable(QCoreApplication::translate("KAboutData CLI", "Please use http://bugs.kde.org to report bugs."))); } else if (!bugAddress().isEmpty()) { printf("%s\n", qPrintable(QCoreApplication::translate("KAboutData CLI", "Please report bugs to %1.").arg(bugAddress()))); } } else { printf("%s\n", qPrintable(customAuthorPlainText())); } } else if (parser->isSet(QStringLiteral("license"))) { foundArgument = true; Q_FOREACH (const KAboutLicense &license, d->_licenseList) { printf("%s\n", qPrintable(license.text())); } } const QString desktopFileName = parser->value(QStringLiteral("desktopfile")); if (!desktopFileName.isEmpty()) { d->desktopFileName = desktopFileName; } if (foundArgument) { ::exit(EXIT_SUCCESS); } } diff --git a/src/lib/randomness/krandomsequence.cpp b/src/lib/randomness/krandomsequence.cpp index 0a59fa2..dc4393c 100644 --- a/src/lib/randomness/krandomsequence.cpp +++ b/src/lib/randomness/krandomsequence.cpp @@ -1,217 +1,217 @@ /* This file is part of the KDE libraries Copyright (c) 1999 Sean Harmer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "krandomsequence.h" #include "krandom.h" -class KRandomSequence::Private +class Q_DECL_HIDDEN KRandomSequence::Private { public: enum {SHUFFLE_TABLE_SIZE = 32}; void draw(); // Generate the random number int lngSeed1; int lngSeed2; int lngShufflePos; int shuffleArray[SHUFFLE_TABLE_SIZE]; }; ////////////////////////////////////////////////////////////////////////////// // Construction / Destruction ////////////////////////////////////////////////////////////////////////////// KRandomSequence::KRandomSequence(long lngSeed1) : d(new Private) { // Seed the generator setSeed(lngSeed1); } KRandomSequence::KRandomSequence(int lngSeed1) : d(new Private) { // Seed the generator setSeed(lngSeed1); } KRandomSequence::~KRandomSequence() { delete d; } KRandomSequence::KRandomSequence(const KRandomSequence &a) : d(new Private) { *d = *a.d; } KRandomSequence &KRandomSequence::operator=(const KRandomSequence &a) { if (this != &a) { *d = *a.d; } return *this; } ////////////////////////////////////////////////////////////////////////////// // Member Functions ////////////////////////////////////////////////////////////////////////////// void KRandomSequence::setSeed(long lngSeed1) { setSeed(static_cast(lngSeed1)); } void KRandomSequence::setSeed(int lngSeed1) { // Convert the positive seed number to a negative one so that the draw() // function can intialise itself the first time it is called. We just have // to make sure that the seed used != 0 as zero perpetuates itself in a // sequence of random numbers. if (lngSeed1 < 0) { d->lngSeed1 = -1; } else if (lngSeed1 == 0) { d->lngSeed1 = -((KRandom::random() & ~1) + 1); } else { d->lngSeed1 = -lngSeed1; } } static const int sMod1 = 2147483563; static const int sMod2 = 2147483399; void KRandomSequence::Private::draw() { static const int sMM1 = sMod1 - 1; static const int sA1 = 40014; static const int sA2 = 40692; static const int sQ1 = 53668; static const int sQ2 = 52774; static const int sR1 = 12211; static const int sR2 = 3791; static const int sDiv = 1 + sMM1 / SHUFFLE_TABLE_SIZE; // Long period (>2 * 10^18) random number generator of L'Ecuyer with // Bayes-Durham shuffle and added safeguards. Returns a uniform random // deviate between 0.0 and 1.0 (exclusive of the endpoint values). Call // with a negative number to initialize; thereafter, do not alter idum // between successive deviates in a sequence. RNMX should approximate // the largest floating point value that is less than 1. int j; // Index for the shuffle table int k; // Initialise if (lngSeed1 <= 0) { lngSeed2 = lngSeed1; // Load the shuffle table after 8 warm-ups for (j = SHUFFLE_TABLE_SIZE + 7; j >= 0; --j) { k = lngSeed1 / sQ1; lngSeed1 = sA1 * (lngSeed1 - k * sQ1) - k * sR1; if (lngSeed1 < 0) { lngSeed1 += sMod1; } if (j < SHUFFLE_TABLE_SIZE) { shuffleArray[j] = lngSeed1; } } lngShufflePos = shuffleArray[0]; } // Start here when not initializing // Compute lngSeed1 = ( lngIA1*lngSeed1 ) % lngIM1 without overflows // by Schrage's method k = lngSeed1 / sQ1; lngSeed1 = sA1 * (lngSeed1 - k * sQ1) - k * sR1; if (lngSeed1 < 0) { lngSeed1 += sMod1; } // Compute lngSeed2 = ( lngIA2*lngSeed2 ) % lngIM2 without overflows // by Schrage's method k = lngSeed2 / sQ2; lngSeed2 = sA2 * (lngSeed2 - k * sQ2) - k * sR2; if (lngSeed2 < 0) { lngSeed2 += sMod2; } j = lngShufflePos / sDiv; lngShufflePos = shuffleArray[j] - lngSeed2; shuffleArray[j] = lngSeed1; if (lngShufflePos < 1) { lngShufflePos += sMM1; } } void KRandomSequence::modulate(int i) { d->lngSeed2 -= i; if (d->lngSeed2 < 0) { d->lngShufflePos += sMod2; } d->draw(); d->lngSeed1 -= i; if (d->lngSeed1 < 0) { d->lngSeed1 += sMod1; } d->draw(); } double KRandomSequence::getDouble() { static const double finalAmp = 1.0 / double(sMod1); static const double epsilon = 1.2E-7; static const double maxRand = 1.0 - epsilon; double temp; d->draw(); // Return a value that is not one of the endpoints if ((temp = finalAmp * d->lngShufflePos) > maxRand) { // We don't want to return 1.0 return maxRand; } else { return temp; } } unsigned long KRandomSequence::getLong(unsigned long max) { return getInt(static_cast(max)); } unsigned int KRandomSequence::getInt(unsigned int max) { d->draw(); return max ? ((static_cast(d->lngShufflePos)) % max) : 0; } bool KRandomSequence::getBool() { d->draw(); return ((static_cast(d->lngShufflePos)) & 1); } diff --git a/src/lib/util/kdelibs4configmigrator.cpp b/src/lib/util/kdelibs4configmigrator.cpp index 5b05be8..3f55c44 100644 --- a/src/lib/util/kdelibs4configmigrator.cpp +++ b/src/lib/util/kdelibs4configmigrator.cpp @@ -1,133 +1,133 @@ /* This file is part of the KDE Frameworks Copyright 2014 Montel Laurent This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kdelibs4configmigrator.h" #include #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(MIGRATOR) // logging category for this framework, default: log stuff >= warning Q_LOGGING_CATEGORY(MIGRATOR, "kf5.kcoreaddons.kdelibs4configmigrator", QtWarningMsg) -class Kdelibs4ConfigMigrator::Private +class Q_DECL_HIDDEN Kdelibs4ConfigMigrator::Private { public: Private(const QString &_appName) : appName(_appName) { } QStringList configFiles; QStringList uiFiles; QString appName; }; Kdelibs4ConfigMigrator::Kdelibs4ConfigMigrator(const QString &appName) : d(new Private(appName)) { } Kdelibs4ConfigMigrator::~Kdelibs4ConfigMigrator() { delete d; } void Kdelibs4ConfigMigrator::setConfigFiles(const QStringList &configFileNameList) { d->configFiles = configFileNameList; } void Kdelibs4ConfigMigrator::setUiFiles(const QStringList &uiFileNameList) { d->uiFiles = uiFileNameList; } bool Kdelibs4ConfigMigrator::migrate() { // Testing for kdehome Kdelibs4Migration migration; if (!migration.kdeHomeFound()) { return false; } bool didSomething = false; Q_FOREACH (const QString &configFileName, d->configFiles) { const QString newConfigLocation = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + configFileName; if (QFile(newConfigLocation).exists()) { continue; } //Be safe QFileInfo fileInfo(newConfigLocation); QDir().mkpath(fileInfo.absolutePath()); const QString oldConfigFile(migration.locateLocal("config", configFileName)); if (!oldConfigFile.isEmpty()) { if (QFile(oldConfigFile).copy(newConfigLocation)) { didSomething = true; qCDebug(MIGRATOR) << "config file" << oldConfigFile << "was migrated to" << newConfigLocation; } } } if (d->appName.isEmpty() && !d->uiFiles.isEmpty()) { qCritical() << " We can not migrate ui file. AppName is missing"; } else { Q_FOREACH (const QString &uiFileName, d->uiFiles) { const QString newConfigLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kxmlgui5/") + d->appName + QLatin1Char('/') + uiFileName; if (QFile(newConfigLocation).exists()) { continue; } QFileInfo fileInfo(newConfigLocation); QDir().mkpath(fileInfo.absolutePath()); const QString oldConfigFile(migration.locateLocal("data", d->appName + QLatin1Char('/') + uiFileName)); if (!oldConfigFile.isEmpty()) { if (QFile(oldConfigFile).copy(newConfigLocation)) { didSomething = true; qCDebug(MIGRATOR) << "ui file" << oldConfigFile << "was migrated to" << newConfigLocation; } } } } // Trigger KSharedConfig::openConfig()->reparseConfiguration() via the framework integration plugin if (didSomething) { QPluginLoader lib(QStringLiteral("kf5/FrameworkIntegrationPlugin")); QObject *rootObj = lib.instance(); if (rootObj) { QMetaObject::invokeMethod(rootObj, "reparseConfiguration"); } } return true; } diff --git a/src/lib/util/kuser_unix.cpp b/src/lib/util/kuser_unix.cpp index d0ba6cb..c6da2c3 100644 --- a/src/lib/util/kuser_unix.cpp +++ b/src/lib/util/kuser_unix.cpp @@ -1,563 +1,563 @@ /* * KUser - represent a user/account * Copyright (C) 2002 Tim Jansen * Copyright (C) 2014 Alex Richardson * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kuser.h" #include "config-getgrouplist.h" #include "config-accountsservice.h" #include #include #include #include #include #include #include #include // std::find #include // std::function #if defined(__BIONIC__) static inline struct passwd * getpwent() { return nullptr; } inline void setpwent() { } static inline void setgrent() { } static inline struct group * getgrent() { return nullptr; } inline void endpwent() { } static inline void endgrent() { } #endif -class KUser::Private : public QSharedData +class Q_DECL_HIDDEN KUser::Private : public QSharedData { public: uid_t uid; gid_t gid; QString loginName; QString homeDir, shell; QMap properties; Private() : uid(uid_t(-1)), gid(gid_t(-1)) {} Private(const char *name) : uid(uid_t(-1)), gid(gid_t(-1)) { fillPasswd(name ? ::getpwnam(name) : nullptr); } Private(const passwd *p) : uid(uid_t(-1)), gid(gid_t(-1)) { fillPasswd(p); } void fillPasswd(const passwd *p) { if (p) { #ifndef __BIONIC__ QString gecos = QString::fromLocal8Bit(p->pw_gecos); #else QString gecos = QString(); #endif QStringList gecosList = gecos.split(QLatin1Char(',')); // fill up the list, should be at least 4 entries while (gecosList.size() < 4) { gecosList << QString(); } uid = p->pw_uid; gid = p->pw_gid; loginName = QString::fromLocal8Bit(p->pw_name); properties[KUser::FullName] = QVariant(gecosList[0]); properties[KUser::RoomNumber] = QVariant(gecosList[1]); properties[KUser::WorkPhone] = QVariant(gecosList[2]); properties[KUser::HomePhone] = QVariant(gecosList[3]); if (uid == ::getuid() && uid == ::geteuid()) { homeDir = QFile::decodeName(qgetenv("HOME")); } if (homeDir.isEmpty()) { homeDir = QFile::decodeName(p->pw_dir); } shell = QString::fromLocal8Bit(p->pw_shell); } } }; KUser::KUser(UIDMode mode) { uid_t _uid = ::getuid(), _euid; if (mode == UseEffectiveUID && (_euid = ::geteuid()) != _uid) { d = new Private(::getpwuid(_euid)); } else { d = new Private(qgetenv("LOGNAME").constData()); if (d->uid != _uid) { d = new Private(qgetenv("USER").constData()); if (d->uid != _uid) { d = new Private(::getpwuid(_uid)); } } } } KUser::KUser(K_UID _uid) : d(new Private(::getpwuid(_uid))) { } KUser::KUser(KUserId _uid) : d(new Private(::getpwuid(_uid.nativeId()))) { } KUser::KUser(const QString &name) : d(new Private(name.toLocal8Bit().data())) { } KUser::KUser(const char *name) : d(new Private(name)) { } KUser::KUser(const passwd *p) : d(new Private(p)) { } KUser::KUser(const KUser &user) : d(user.d) { } KUser &KUser::operator =(const KUser &user) { d = user.d; return *this; } bool KUser::operator ==(const KUser &user) const { return isValid() && (d->uid == user.d->uid); } bool KUser::isValid() const { return d->uid != uid_t(-1); } KUserId KUser::userId() const { return KUserId(d->uid); } KGroupId KUser::groupId() const { return KGroupId(d->gid); } bool KUser::isSuperUser() const { return d->uid == 0; } QString KUser::loginName() const { return d->loginName; } QString KUser::homeDir() const { return d->homeDir; } QString KUser::faceIconPath() const { QString pathToFaceIcon; if (!d->loginName.isEmpty()) { pathToFaceIcon = QStringLiteral(ACCOUNTS_SERVICE_ICON_DIR) + QLatin1Char('/') + d->loginName; } if (QFile::exists(pathToFaceIcon)) { return pathToFaceIcon; } pathToFaceIcon = QString(homeDir() + QLatin1Char('/') + QStringLiteral(".face.icon")); if (QFile::exists(pathToFaceIcon)) { return pathToFaceIcon; } return QString(); } QString KUser::shell() const { return d->shell; } template static void listGroupsForUser(const char *name, gid_t gid, uint maxCount, Func handleNextGroup) { if (Q_UNLIKELY(maxCount == 0)) { return; } uint found = 0; #if HAVE_GETGROUPLIST QVarLengthArray gid_buffer; gid_buffer.resize(100); int numGroups = gid_buffer.size(); int result = getgrouplist(name, gid, gid_buffer.data(), &numGroups); if (result < 0 && uint(numGroups) < maxCount) { // getgrouplist returns -1 if the buffer was too small to store all entries, the required size is in numGroups qDebug("Buffer was too small: %d, need %d", gid_buffer.size(), numGroups); gid_buffer.resize(numGroups); numGroups = gid_buffer.size(); getgrouplist(name, gid, gid_buffer.data(), &numGroups); } for (int i = 0; i < numGroups && found < maxCount; ++i) { struct group *g = getgrgid(gid_buffer[i]); // should never be null, but better be safe than crash if (g) { found++; handleNextGroup(g); } } #else // fall back to getgrent() and reading gr->gr_mem // This is slower than getgrouplist, but works as well // add the current gid, this is often not part of g->gr_mem (e.g. build.kde.org or my openSuSE 13.1 system) struct group *g = getgrgid(gid); if (g) { handleNextGroup(g); found++; if (found >= maxCount) { return; } } static const auto groupContainsUser = [](struct group * g, const char *name) -> bool { for (char **user = g->gr_mem; *user; user++) { if (strcmp(name, *user) == 0) { return true; } } return false; }; setgrent(); while ((g = getgrent())) { // don't add the current gid again if (g->gr_gid != gid && groupContainsUser(g, name)) { handleNextGroup(g); found++; if (found >= maxCount) { break; } } } endgrent(); #endif } QList KUser::groups(uint maxCount) const { QList result; listGroupsForUser( d->loginName.toLocal8Bit().constData(), d->gid, maxCount, [&](const group * g) { result.append(KUserGroup(g)); } ); return result; } QStringList KUser::groupNames(uint maxCount) const { QStringList result; listGroupsForUser( d->loginName.toLocal8Bit().constData(), d->gid, maxCount, [&](const group * g) { result.append(QString::fromLocal8Bit(g->gr_name)); } ); return result; } QVariant KUser::property(UserProperty which) const { return d->properties.value(which); } QList KUser::allUsers(uint maxCount) { QList result; passwd *p; setpwent(); for (uint i = 0; i < maxCount && (p = getpwent()); ++i) { result.append(KUser(p)); } endpwent(); return result; } QStringList KUser::allUserNames(uint maxCount) { QStringList result; passwd *p; setpwent(); for (uint i = 0; i < maxCount && (p = getpwent()); ++i) { result.append(QString::fromLocal8Bit(p->pw_name)); } endpwent(); return result; } KUser::~KUser() { } -class KUserGroup::Private : public QSharedData +class Q_DECL_HIDDEN KUserGroup::Private : public QSharedData { public: gid_t gid; QString name; Private() : gid(gid_t(-1)) {} Private(const char *_name) : gid(gid_t(-1)) { fillGroup(_name ? ::getgrnam(_name) : nullptr); } Private(const ::group *p) : gid(gid_t(-1)) { fillGroup(p); } void fillGroup(const ::group *p) { if (p) { gid = p->gr_gid; name = QString::fromLocal8Bit(p->gr_name); } } }; KUserGroup::KUserGroup(KUser::UIDMode mode) { d = new Private(getgrgid(KUser(mode).groupId().nativeId())); } KUserGroup::KUserGroup(K_GID _gid) : d(new Private(getgrgid(_gid))) { } KUserGroup::KUserGroup(KGroupId _gid) : d(new Private(getgrgid(_gid.nativeId()))) { } KUserGroup::KUserGroup(const QString &_name) : d(new Private(_name.toLocal8Bit().data())) { } KUserGroup::KUserGroup(const char *_name) : d(new Private(_name)) { } KUserGroup::KUserGroup(const ::group *g) : d(new Private(g)) { } KUserGroup::KUserGroup(const KUserGroup &group) : d(group.d) { } KUserGroup &KUserGroup::operator =(const KUserGroup &group) { d = group.d; return *this; } bool KUserGroup::operator ==(const KUserGroup &group) const { return isValid() && (d->gid == group.d->gid); } bool KUserGroup::isValid() const { return d->gid != gid_t(-1); } KGroupId KUserGroup::groupId() const { return KGroupId(d->gid); } QString KUserGroup::name() const { return d->name; } static void listGroupMembers(gid_t gid, uint maxCount, std::function handleNextGroupUser) { if (maxCount == 0) { return; } struct group *g = getgrgid(gid); if (!g) { return; } uint found = 0; QVarLengthArray addedUsers; struct passwd *p = nullptr; for (char **user = g->gr_mem; *user; user++) { if ((p = getpwnam(*user))) { addedUsers.append(p->pw_uid); handleNextGroupUser(p); found++; if (found >= maxCount) { break; } } } //gr_mem doesn't contain users where the primary group == gid -> we have to iterate over all users setpwent(); while ((p = getpwent()) && found < maxCount) { if (p->pw_gid != gid) { continue; // only need primary gid since otherwise gr_mem already contains this user } // make sure we don't list a user twice if (std::find(addedUsers.cbegin(), addedUsers.cend(), p->pw_uid) == addedUsers.cend()) { handleNextGroupUser(p); found++; } } endpwent(); } QList KUserGroup::users(uint maxCount) const { QList result; listGroupMembers(d->gid, maxCount, [&](const passwd *p) { result.append(KUser(p)); }); return result; } QStringList KUserGroup::userNames(uint maxCount) const { QStringList result; listGroupMembers(d->gid, maxCount, [&](const passwd *p) { result.append(QString::fromLocal8Bit(p->pw_name)); }); return result; } QList KUserGroup::allGroups(uint maxCount) { QList result; ::group *g; setgrent(); for (uint i = 0; i < maxCount && (g = getgrent()); ++i) { result.append(KUserGroup(g)); } endgrent(); return result; } QStringList KUserGroup::allGroupNames(uint maxCount) { QStringList result; ::group *g; setgrent(); for (uint i = 0; i < maxCount && (g = getgrent()); ++i) { result.append(QString::fromLocal8Bit(g->gr_name)); } endgrent(); return result; } KUserGroup::~KUserGroup() { } KUserId KUserId::fromName(const QString &name) { if (name.isEmpty()) { return KUserId(); } QByteArray name8Bit = name.toLocal8Bit(); struct passwd *p = ::getpwnam(name8Bit.constData()); if (!p) { qWarning("Failed to lookup user %s: %s", name8Bit.constData(), strerror(errno)); return KUserId(); } return KUserId(p->pw_uid); } KGroupId KGroupId::fromName(const QString &name) { if (name.isEmpty()) { return KGroupId(); } QByteArray name8Bit = name.toLocal8Bit(); struct group *g = ::getgrnam(name8Bit.constData()); if (!g) { qWarning("Failed to lookup group %s: %s", name8Bit.constData(), strerror(errno)); return KGroupId(); } return KGroupId(g->gr_gid); } KUserId KUserId::currentUserId() { return KUserId(getuid()); } KUserId KUserId::currentEffectiveUserId() { return KUserId(geteuid()); } KGroupId KGroupId::currentGroupId() { return KGroupId(getgid()); } KGroupId KGroupId::currentEffectiveGroupId() { return KGroupId(getegid()); } diff --git a/src/lib/util/kuser_win.cpp b/src/lib/util/kuser_win.cpp index 5c3fa45..915adbd 100644 --- a/src/lib/util/kuser_win.cpp +++ b/src/lib/util/kuser_win.cpp @@ -1,919 +1,919 @@ /* * KUser - represent a user/account (Windows) * Copyright (C) 2007 Bernhard Loos * Copyright (C) 2014 Alex Richardson * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kuser.h" #include "kcoreaddons_debug.h" #include #include #include // unique_ptr #include #include #include //Net* #include //GetProfilesDirectoryW #include //ConvertSidToStringSidW #include // this can't be a lambda due to a MSVC2012 bug // (works fine in 2010 and 2013) struct netApiBufferDeleter { void operator()(void *buffer) { if (buffer) { NetApiBufferFree(buffer); } } }; template class ScopedNetApiBuffer : public std::unique_ptr { public: // explicit scope resolution operator needed in ::netApiBufferDeleter // because of *another* MSVC bug :( inline explicit ScopedNetApiBuffer(T *data) : std::unique_ptr(data, ::netApiBufferDeleter()) {} }; const auto handleCloser = [](HANDLE h) { if (h != INVALID_HANDLE_VALUE) { CloseHandle(h); } }; typedef std::unique_ptr::type, decltype(handleCloser)> ScopedHANDLE; /** Make sure the NetApi functions are called with the correct level argument (for template functions) * This argument can be retrieved by using NetApiTypeInfo::level. In order to do so the type must be * registered by writing e.g. NETAPI_TYPE_INFO(GROUP_INFO, 0) for GROUP_INFO_0 */ template struct NetApiTypeInfo {}; #define NETAPI_TYPE_INFO(prefix, n) template<> struct NetApiTypeInfo { enum { level = n }; }; NETAPI_TYPE_INFO(GROUP_INFO, 0); NETAPI_TYPE_INFO(GROUP_INFO, 3); NETAPI_TYPE_INFO(USER_INFO, 0); NETAPI_TYPE_INFO(USER_INFO, 4); NETAPI_TYPE_INFO(USER_INFO, 11); NETAPI_TYPE_INFO(GROUP_USERS_INFO, 0); // T must be a USER_INFO_* structure template ScopedNetApiBuffer getUserInfo(LPCWSTR server, const QString &userName, NET_API_STATUS *errCode) { LPBYTE userInfoTmp = nullptr; // if level = 11 a USER_INFO_11 structure gets filled in and allocated by NetUserGetInfo(), etc. NET_API_STATUS status = NetUserGetInfo(server, (LPCWSTR)userName.utf16(), NetApiTypeInfo::level, &userInfoTmp); if (status != NERR_Success) { userInfoTmp = nullptr; } if (errCode) { *errCode = status; } return ScopedNetApiBuffer((T*)userInfoTmp); } //enumeration functions /** simplify calling the Net*Enum functions to prevent copy and paste for allUsers(), allUserNames(), allGroups(), allGroupNames() * @tparam T The type that is enumerated (e.g. USER_INFO_11) Must be registered using NETAPI_TYPE_INFO. * @param callback Callback for each listed object. Signature: void(const T&) * @param enumFunc This function enumerates the data using a Net* function. * It will be called in a loop as long as it returns ERROR_MORE_DATA. * */ template static void netApiEnumerate(uint maxCount, Callback callback, EnumFunction enumFunc) { NET_API_STATUS nStatus = NERR_Success; DWORD_PTR resumeHandle = 0; uint total = 0; int level = NetApiTypeInfo::level; do { LPBYTE buffer = nullptr; DWORD entriesRead = 0; DWORD totalEntries = 0; nStatus = enumFunc(level, &buffer, &entriesRead, &totalEntries, &resumeHandle); //qDebug("Net*Enum(level = %d) returned %d entries, total was (%d), status = %d, resume handle = %llx", // level, entriesRead, totalEntries, nStatus, resumeHandle); // buffer must always be freed, even if Net*Enum fails ScopedNetApiBuffer groupInfo((T*)buffer); if (nStatus == NERR_Success || nStatus == ERROR_MORE_DATA) { for (DWORD i = 0; total < maxCount && i < entriesRead; i++, total++) { callback(groupInfo.get()[i]); } } else { qWarning("NetApi enumerate function failed: status = %d", (int)nStatus); } } while (nStatus == ERROR_MORE_DATA); } template void enumerateAllUsers(uint maxCount, Callback callback) { netApiEnumerate(maxCount, callback, [](int level, LPBYTE *buffer, DWORD *count, DWORD *total, PDWORD_PTR resumeHandle) { // pass 0 as filter -> get all users // Why does this function take a DWORD* as resume handle and NetUserEnum/NetGroupGetUsers a UINT64* // Great API design by Microsoft... //casting the uint64* to uint32* is fine, it just writes to the first 32 bits return NetUserEnum(nullptr, level, 0, buffer, MAX_PREFERRED_LENGTH, count, total, (PDWORD)resumeHandle); }); } template void enumerateAllGroups(uint maxCount, Callback callback) { netApiEnumerate(maxCount, callback, [](int level, LPBYTE *buffer, DWORD *count, DWORD *total, PDWORD_PTR resumeHandle) { return NetGroupEnum(nullptr, level, buffer, MAX_PREFERRED_LENGTH, count, total, resumeHandle); }); } template void enumerateGroupsForUser(uint maxCount, const QString &name, Callback callback) { LPCWSTR nameStr = (LPCWSTR)name.utf16(); netApiEnumerate(maxCount, callback, [&](int level, LPBYTE *buffer, DWORD *count, DWORD *total, PDWORD_PTR resumeHandle) -> NET_API_STATUS { Q_UNUSED(resumeHandle); NET_API_STATUS ret = NetUserGetGroups(nullptr, nameStr, level, buffer, MAX_PREFERRED_LENGTH, count, total); // if we return ERROR_MORE_DATA here it will result in an enless loop if (ret == ERROR_MORE_DATA) { qCWarning(KCOREADDONS_DEBUG) << "NetUserGetGroups for user" << name << "returned ERROR_MORE_DATA. This should not happen!"; ret = NERR_Success; } return ret; }); } template void enumerateUsersForGroup(const QString &name, uint maxCount, Callback callback) { LPCWSTR nameStr = (LPCWSTR)name.utf16(); netApiEnumerate(maxCount, callback, [nameStr](int level, LPBYTE *buffer, DWORD *count, DWORD *total, PDWORD_PTR resumeHandle) { return NetGroupGetUsers(nullptr, nameStr, level, buffer, MAX_PREFERRED_LENGTH, count, total, resumeHandle); }); } -class KUser::Private : public QSharedData +class Q_DECL_HIDDEN KUser::Private : public QSharedData { typedef QExplicitlySharedDataPointer Ptr; Private() : isAdmin(false) {} //takes ownership over userInfo_ Private(KUserId uid, KGroupId gid, const QString &loginName, const QString &fullName, const QString &domain, const QString &homeDir, bool isAdmin) : uid(uid), gid(gid), loginName(loginName), fullName(fullName), domain(domain), homeDir(homeDir), isAdmin(isAdmin) { Q_ASSERT(uid.isValid()); } static QString guessHomeDir(const QString &username, KUserId uid) { // usri11_home_dir/usri4_home_dir is often empty // check whether it is the homedir for the current user and if not then fall back to "\" if (uid == KUserId::currentUserId()) { return QDir::homePath(); } QString homeDir; WCHAR profileDirPath[MAX_PATH]; DWORD bufSize = MAX_PATH; BOOL result = GetProfilesDirectoryW(profileDirPath, &bufSize); if (result) { // This might not be correct: e.g. with local user and domain user with same // In that case it could be C:\Users\Foo (local user) vs C:\Users\Foo.DOMAIN (domain user) // However it is still much better than the previous code which just returned the current users home dir homeDir = QString::fromWCharArray(profileDirPath) + QLatin1Char('\\') + username; } return homeDir; } public: static Ptr sharedNull; KUserId uid; KGroupId gid; QString loginName; QString fullName; QString domain; QString homeDir; bool isAdmin; /** Creates a user info from a SID (never returns null) */ static Ptr create(KUserId uid) { if (!uid.isValid()) { return sharedNull; } // now find the fully qualified name for the user DWORD nameBufferLen = UNLEN + 1; WCHAR nameBuffer[UNLEN + 1]; DWORD domainBufferLen = UNLEN + 1; WCHAR domainBuffer[UNLEN + 1]; SID_NAME_USE use; if (!LookupAccountSidW(nullptr, uid.nativeId(), nameBuffer, &nameBufferLen, domainBuffer, &domainBufferLen, &use)) { qCWarning(KCOREADDONS_DEBUG) << "Could not lookup user " << uid.toString() << "error =" << GetLastError(); return sharedNull; } QString loginName = QString::fromWCharArray(nameBuffer); QString domainName = QString::fromWCharArray(domainBuffer); if (use != SidTypeUser && use != SidTypeDeletedAccount) { qCWarning(KCOREADDONS_DEBUG).nospace() << "SID for " << domainName << "\\" << loginName << " (" << uid.toString() << ") is not of type user (" << SidTypeUser << " or " << SidTypeDeletedAccount << "). Got type " << use << " instead."; return sharedNull; } // now get the server name to query (could be null for local machine) LPWSTR servernameTmp = nullptr; NET_API_STATUS status = NetGetAnyDCName(nullptr, 0, (LPBYTE *)&servernameTmp); if (status != NERR_Success) { // this always fails on my desktop system, don't spam the output // qDebug("NetGetAnyDCName failed with error %d", status); } ScopedNetApiBuffer servername(servernameTmp); QString fullName; QString homeDir; KGroupId group; bool isAdmin = false; // must NOT pass the qualified name ("domain\user") here or lookup fails -> just the name // try USER_INFO_4 first, MSDN says it is valid only on servers (whatever that means), it works on my desktop system // If it fails fall back to USER_INFO11, which has all the needed information except primary group if (auto userInfo4 = getUserInfo(servername.get(), loginName, &status)) { Q_ASSERT(KUserId(userInfo4->usri4_user_sid) == uid); // if this is not the same we have a logic error fullName = QString::fromWCharArray(userInfo4->usri4_full_name); homeDir = QString::fromWCharArray(userInfo4->usri4_home_dir); isAdmin = userInfo4->usri4_priv == USER_PRIV_ADMIN; // now determine the primary group: const DWORD primaryGroup = userInfo4->usri4_primary_group_id; // primary group is a relative identifier, i.e. in order to get the SID for that group // we have to take the user SID and replace the last subauthority value with the relative identifier group = KGroupId(uid.nativeId()); // constructor does not check whether the sid refers to a group Q_ASSERT(group.isValid()); UCHAR numSubauthorities = *GetSidSubAuthorityCount(group.nativeId()); PDWORD lastSubAutority = GetSidSubAuthority(group.nativeId(), numSubauthorities - 1); *lastSubAutority = primaryGroup; } else if (auto userInfo11 = getUserInfo(servername.get(), loginName, &status)) { fullName = QString::fromWCharArray(userInfo11->usri11_full_name); homeDir = QString::fromWCharArray(userInfo11->usri11_home_dir); isAdmin = userInfo11->usri11_priv == USER_PRIV_ADMIN; } else { qCWarning(KCOREADDONS_DEBUG).nospace() << "Could not get information for user " << domainName << "\\" << loginName << ": error code = " << status; return sharedNull; } if (homeDir.isEmpty()) { homeDir = guessHomeDir(loginName, uid); } //if we couldn't find a primary group just take the first group found for this user if (!group.isValid()) { enumerateGroupsForUser(1, loginName, [&](const GROUP_USERS_INFO_0 &info) { group = KGroupId::fromName(QString::fromWCharArray(info.grui0_name)); }); } return Ptr(new Private(uid, group, loginName, fullName, domainName, homeDir, isAdmin)); } }; KUser::Private::Ptr KUser::Private::sharedNull(new KUser::Private()); KUser::KUser(UIDMode mode) { if (mode == UseEffectiveUID) { d = Private::create(KUserId::currentEffectiveUserId()); } else if (mode == UseRealUserID) { d = Private::create(KUserId::currentUserId()); } else { d = Private::sharedNull; } } KUser::KUser(K_UID uid) : d(Private::create(KUserId(uid))) { } KUser::KUser(KUserId uid) : d(Private::create(uid)) { } KUser::KUser(const QString &name) : d(Private::create(KUserId::fromName(name))) { } KUser::KUser(const char *name) : d(Private::create(KUserId::fromName(QString::fromLocal8Bit(name)))) { } KUser::KUser(const KUser &user) : d(user.d) { } KUser &KUser::operator=(const KUser &user) { d = user.d; return *this; } bool KUser::operator==(const KUser &user) const { return isValid() && d->uid == user.d->uid; } bool KUser::isValid() const { return d->uid.isValid(); } bool KUser::isSuperUser() const { return d->isAdmin; } QString KUser::loginName() const { return d->loginName; } QString KUser::homeDir() const { return d->homeDir; } // Some RAII objects to help uninitializing/destroying WinAPI stuff // used in faceIconPath. class COMInitializer { public: COMInitializer() : result(CoInitialize(nullptr)) {} ~COMInitializer() { if (SUCCEEDED(result)) { CoUninitialize(); } } HRESULT result; }; class W32Library { public: W32Library(HMODULE h): h(h) {} ~W32Library() { if (h) { FreeLibrary(h); } } operator HMODULE() { return h; } HMODULE h; }; // faceIconPath uses undocumented Windows API known as SHGetUserPicturePath, // only accessible by ordinal, unofficially documented at // http://undoc.airesoft.co.uk/shell32.dll/SHGetUserPicturePath.php // The function has a different ordinal and parameters on Windows XP and Vista/7. // These structs encapsulate the differences. struct FaceIconPath_XP { typedef HRESULT (WINAPI *funcptr_t)(LPCWSTR, DWORD, LPWSTR); static const int ordinal = 233; static HRESULT getPicturePath(funcptr_t SHGetUserPicturePathXP, LPCWSTR username, LPWSTR buf, UINT bufsize) { Q_UNUSED(bufsize); // assumes the buffer is MAX_PATH in size return SHGetUserPicturePathXP(username, 0, buf); } }; struct FaceIconPath_Vista { typedef HRESULT (WINAPI *funcptr_t)(LPCWSTR, DWORD, LPWSTR, UINT); static const int ordinal = 261; static HRESULT getPicturePath(funcptr_t SHGetUserPicturePathV, LPCWSTR username, LPWSTR buf, UINT bufsize) { return SHGetUserPicturePathV(username, 0, buf, bufsize); } }; template static QString faceIconPathImpl(LPCWSTR username) { static COMInitializer COMinit; static W32Library shellMod = LoadLibraryA("shell32.dll"); if (!shellMod) { return QString(); } static typename Platform::funcptr_t sgupp_ptr = reinterpret_cast( GetProcAddress(shellMod, MAKEINTRESOURCEA(Platform::ordinal))); if (!sgupp_ptr) { return QString(); } WCHAR pathBuf[MAX_PATH]; HRESULT res = Platform::getPicturePath(sgupp_ptr, username, pathBuf, MAX_PATH); if (res != S_OK) { return QString(); } return QString::fromWCharArray(pathBuf); } QString KUser::faceIconPath() const { if (!isValid()) { return QString(); } LPCWSTR username = reinterpret_cast(d->loginName.utf16()); if (QSysInfo::windowsVersion() == QSysInfo::WV_XP || QSysInfo::windowsVersion() == QSysInfo::WV_2003) { return faceIconPathImpl(username); } else if (QSysInfo::windowsVersion() >= QSysInfo::WV_VISTA) { return faceIconPathImpl(username); } return QString(); } QString KUser::shell() const { return isValid() ? QStringLiteral("cmd.exe") : QString(); } KUserId KUser::userId() const { return d->uid; } KGroupId KUser::groupId() const { return d->gid; } QVariant KUser::property(UserProperty which) const { if (which == FullName) { return QVariant(d->fullName); } return QVariant(); } KUser::~KUser() { } -class KUserGroup::Private : public QSharedData +class Q_DECL_HIDDEN KUserGroup::Private : public QSharedData { public: QString name; KGroupId gid; Private() {} Private(const QString &name, KGroupId id) : name(name), gid(id) { if (!name.isEmpty()) { PBYTE groupInfoTmp = nullptr; NET_API_STATUS status = NetGroupGetInfo(nullptr, (LPCWSTR)name.utf16(), 0, &groupInfoTmp); // must always be freed, even on error ScopedNetApiBuffer groupInfo((GROUP_INFO_0 *)groupInfoTmp); if (status != NERR_Success) { qCWarning(KCOREADDONS_DEBUG) << "Failed to find group with name" << name << "error =" << status; groupInfo.reset(); } if (!id.isValid()) { gid = KGroupId::fromName(name); } } } }; KUserGroup::KUserGroup(const QString &_name) : d(new Private(_name, KGroupId())) { } KUserGroup::KUserGroup(const char *_name) : d(new Private(QLatin1String(_name), KGroupId())) { } static QString nameFromGroupId(KGroupId gid) { if (!gid.isValid()) { return QString(); } DWORD bufferLen = UNLEN + 1; WCHAR buffer[UNLEN + 1]; DWORD domainBufferLen = UNLEN + 1; WCHAR domainBuffer[UNLEN + 1]; SID_NAME_USE eUse; QString name; if (LookupAccountSidW(NULL, gid.nativeId(), buffer, &bufferLen, domainBuffer, &domainBufferLen, &eUse)) { if (eUse == SidTypeGroup || eUse == SidTypeWellKnownGroup) { name = QString::fromWCharArray(buffer); } else { qCWarning(KCOREADDONS_DEBUG) << QString::fromWCharArray(buffer) << "is not a group, SID type is" << eUse; } } return name; } KUserGroup::KUserGroup(KGroupId gid) : d(new Private(nameFromGroupId(gid), gid)) { } KUserGroup::KUserGroup(K_GID gid) { KGroupId groupId(gid); d = new Private(nameFromGroupId(groupId), groupId); } KUserGroup::KUserGroup(KUser::UIDMode mode) { KGroupId gid; if (mode == KUser::UseEffectiveUID) { gid = KGroupId::currentGroupId(); } else if (mode == KUser::UseRealUserID) { gid = KGroupId::currentEffectiveGroupId(); } d = new Private(nameFromGroupId(gid), gid); } KUserGroup::KUserGroup(const KUserGroup &group) : d(group.d) { } KUserGroup &KUserGroup::operator =(const KUserGroup &group) { d = group.d; return *this; } bool KUserGroup::operator==(const KUserGroup &group) const { return isValid() && d->gid == group.d->gid && d->name == group.d->name; } bool KUserGroup::isValid() const { return d->gid.isValid() && !d->name.isEmpty(); } QString KUserGroup::name() const { return d->name; } KGroupId KUserGroup::groupId() const { return d->gid; } KUserGroup::~KUserGroup() { } QList KUser::allUsers(uint maxCount) { QList result; // No advantage if we take a USER_INFO_11, since there is no way of copying it // and it is not owned by this function! // -> get a USER_INFO_0 instead and then use KUser(QString) // USER_INFO_23 or USER_INFO_23 would be ideal here since they contains a SID, // but that fails with error code 0x7c (bad level) enumerateAllUsers(maxCount, [&result](const USER_INFO_0 &info) { result.append(KUser(QString::fromWCharArray(info.usri0_name))); }); return result; } QStringList KUser::allUserNames(uint maxCount) { QStringList result; enumerateAllUsers(maxCount, [&result](const USER_INFO_0 &info) { result.append(QString::fromWCharArray(info.usri0_name)); }); return result; } QList KUserGroup::allGroups(uint maxCount) { QList result; // MSDN documentation say 3 is a valid level, however the function fails with invalid level!!! // User GROUP_INFO_0 instead... enumerateAllGroups(maxCount, [&result](const GROUP_INFO_0 &groupInfo) { result.append(KUserGroup(QString::fromWCharArray(groupInfo.grpi0_name))); }); return result; } QStringList KUserGroup::allGroupNames(uint maxCount) { QStringList result; enumerateAllGroups(maxCount, [&result](const GROUP_INFO_0 &groupInfo) { result.append(QString::fromWCharArray(groupInfo.grpi0_name)); }); return result; } QList KUser::groups(uint maxCount) const { QList result; if (!isValid()) { return result; } enumerateGroupsForUser(maxCount, d->loginName, [&result](const GROUP_USERS_INFO_0 &info) { result.append(KUserGroup(QString::fromWCharArray(info.grui0_name))); }); return result; } QStringList KUser::groupNames(uint maxCount) const { QStringList result; if (!isValid()) { return result; } enumerateGroupsForUser(maxCount, d->loginName, [&result](const GROUP_USERS_INFO_0 &info) { result.append(QString::fromWCharArray(info.grui0_name)); }); return result; } QList KUserGroup::users(uint maxCount) const { QList result; if (!isValid()) { return result; } enumerateGroupsForUser(maxCount, d->name, [&result](const GROUP_USERS_INFO_0 &info) { result.append(KUser(QString::fromWCharArray(info.grui0_name))); }); return result; } QStringList KUserGroup::userNames(uint maxCount) const { QStringList result; if (!isValid()) { return result; } enumerateGroupsForUser(maxCount, d->name, [&result](const GROUP_USERS_INFO_0 &info) { result.append(QString::fromWCharArray(info.grui0_name)); }); return result; } static const auto invalidSidString = QStringLiteral(""); static QString sidToString(void *sid) { if (!sid || !IsValidSid(sid)) { return invalidSidString; } WCHAR *sidStr; // allocated by ConvertStringSidToSidW, must be freed using LocalFree() if (!ConvertSidToStringSidW(sid, &sidStr)) { return invalidSidString; } QString ret = QString::fromWCharArray(sidStr); LocalFree(sidStr); return ret; } struct WindowsSIDWrapper : public QSharedData { char sidBuffer[SECURITY_MAX_SID_SIZE]; /** @return a copy of @p sid or null if sid is not valid or an error occurs */ static WindowsSIDWrapper *copySid(PSID sid) { if (!sid || !IsValidSid(sid)) { return nullptr; } //create a copy of sid WindowsSIDWrapper *copy = new WindowsSIDWrapper(); bool success = CopySid(SECURITY_MAX_SID_SIZE, copy->sidBuffer, sid); if (!success) { QString sidString = sidToString(sid); qWarning("Failed to copy SID %s, error = %d", qPrintable(sidString), (int)GetLastError()); delete copy; return nullptr; } return copy; } }; template<> KUserOrGroupId::KUserOrGroupId() { } template<> KUserOrGroupId::~KUserOrGroupId() { } template<> KUserOrGroupId::KUserOrGroupId(const KUserOrGroupId &other) : data(other.data) { } template<> inline KUserOrGroupId &KUserOrGroupId::operator=(const KUserOrGroupId &other) { data = other.data; return *this; } template<> KUserOrGroupId::KUserOrGroupId(void *nativeId) : data(WindowsSIDWrapper::copySid(nativeId)) { } template<> bool KUserOrGroupId::isValid() const { return data; } template<> void *KUserOrGroupId::nativeId() const { if (!data) { return nullptr; } return data->sidBuffer; } template<> bool KUserOrGroupId::operator==(const KUserOrGroupId &other) const { if (data) { if (!other.data) { return false; } return EqualSid(data->sidBuffer, other.data->sidBuffer); } return !other.data; //only equal if other data is also invalid } template<> bool KUserOrGroupId::operator!=(const KUserOrGroupId &other) const { return !(*this == other); } template<> QString KUserOrGroupId::toString() const { return sidToString(data ? data->sidBuffer : nullptr); } /** T must be either KUserId or KGroupId, Callback has signature T(PSID, SID_NAME_USE) */ template static T sidFromName(const QString &name, Callback callback) { if (name.isEmpty()) { // for some reason empty string will always return S-1-5-32 which is of type domain // we only want users or groups -> return invalid return T(); } char buffer[SECURITY_MAX_SID_SIZE]; DWORD sidLength = SECURITY_MAX_SID_SIZE; // ReferencedDomainName must be passed or LookupAccountNameW fails // Documentation says it is optional, however if not passed the function fails and returns the required size WCHAR domainBuffer[1024]; DWORD domainBufferSize = 1024; SID_NAME_USE sidType; bool ok = LookupAccountNameW(nullptr, (LPCWSTR)name.utf16(), buffer, &sidLength, domainBuffer, &domainBufferSize, &sidType); if (!ok) { qCWarning(KCOREADDONS_DEBUG) << "Failed to lookup account" << name << "error code =" << GetLastError(); return T(); } return callback(buffer, sidType); } KUserId KUserId::fromName(const QString &name) { return sidFromName(name, [&](PSID sid, SID_NAME_USE sidType) -> KUserId { if (sidType != SidTypeUser && sidType != SidTypeDeletedAccount) { qCWarning(KCOREADDONS_DEBUG).nospace() << "Failed to lookup user name " << name << ": resulting SID " << sidToString(sid) << " is not a user." " Got SID type " << sidType << " instead."; return KUserId(); } return KUserId(sid); }); } KGroupId KGroupId::fromName(const QString &name) { return sidFromName(name, [&](PSID sid, SID_NAME_USE sidType) -> KGroupId { if (sidType != SidTypeGroup && sidType != SidTypeWellKnownGroup) { qCWarning(KCOREADDONS_DEBUG).nospace() << "Failed to lookup user name " << name << ": resulting SID " << sidToString(sid) << " is not a group." " Got SID type " << sidType << " instead."; return KGroupId(); } return KGroupId(sid); }); } static std::unique_ptr queryProcessInformation(TOKEN_INFORMATION_CLASS type) { HANDLE _token; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &_token)) { qWarning("Failed to get the token for the current process: %d", (int)GetLastError()); return nullptr; } ScopedHANDLE token(_token, handleCloser); // query required size DWORD requiredSize; if (!GetTokenInformation(token.get(), type, nullptr, 0, &requiredSize)) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { qWarning("Failed to get the required size for the token information %d: %d", type, (int)GetLastError()); return nullptr; } } std::unique_ptr buffer(new char[requiredSize]); if (!GetTokenInformation(token.get(), type, buffer.get(), requiredSize, &requiredSize)) { qWarning("Failed to get token information %d from current process: %d", type, (int)GetLastError()); return nullptr; } return buffer; } KUserId KUserId::currentUserId() { std::unique_ptr userTokenBuffer = queryProcessInformation(TokenUser); TOKEN_USER *userToken = (TOKEN_USER *)userTokenBuffer.get(); return KUserId(userToken->User.Sid); } KGroupId KGroupId::currentGroupId() { std::unique_ptr primaryGroupBuffer = queryProcessInformation(TokenPrimaryGroup); TOKEN_PRIMARY_GROUP *primaryGroup = (TOKEN_PRIMARY_GROUP *)primaryGroupBuffer.get(); return KGroupId(primaryGroup->PrimaryGroup); } KUserId KUserId::currentEffectiveUserId() { return currentUserId(); } KGroupId KGroupId::currentEffectiveGroupId() { return currentGroupId(); } KCOREADDONS_EXPORT uint qHash(const KUserId &id, uint seed) { if (!id.isValid()) { return seed; } // we can't just hash the pointer since equal object must have the same hash -> hash contents char *sid = (char *)id.nativeId(); return qHash(QByteArray::fromRawData(sid, GetLengthSid(sid)), seed); } KCOREADDONS_EXPORT uint qHash(const KGroupId &id, uint seed) { if (!id.isValid()) { return seed; } // we can't just hash the pointer since equal object must have the same hash -> hash contents char *sid = (char *)id.nativeId(); return qHash(QByteArray::fromRawData(sid, GetLengthSid(sid)), seed); }