File indexing completed on 2024-05-05 17:54:18
0001 // SPDX-FileCopyrightText: 2020 Simon Persson <simon.persson@mykolab.com> 0002 // 0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 0005 #include "bupvfs.h" 0006 #include "kupkio_debug.h" 0007 0008 #include <git2/blob.h> 0009 #include <git2/branch.h> 0010 0011 #include <sys/stat.h> 0012 0013 #include <QMimeDatabase> 0014 0015 git_revwalk *Node::mRevisionWalker = nullptr; 0016 git_repository *Node::mRepository = nullptr; 0017 0018 Node::Node(QObject *pParent, const QString &pName, qint64 pMode) 0019 :QObject(pParent), Metadata(pMode) 0020 { 0021 setObjectName(pName); 0022 } 0023 0024 int Node::readMetadata(VintStream &pMetadataStream) { 0025 return ::readMetadata(pMetadataStream, *this); 0026 } 0027 0028 Node *Node::resolve(const QString &pPath, bool pFollowLinks) { 0029 Node *lParentNode = this; 0030 QString lTarget = pPath; 0031 if(lTarget.startsWith(QLatin1Char('/'))) { 0032 lTarget.remove(0, 1); 0033 lParentNode = parentCommit(); 0034 } 0035 return lParentNode->resolve(lTarget.split(QLatin1Char('/'), Qt::SkipEmptyParts), pFollowLinks); 0036 } 0037 0038 Node *Node::resolve(const QStringList &pPathList, bool pFollowLinks) { 0039 Node *lNode = this; 0040 foreach(QString lPathComponent, pPathList) { 0041 if(lPathComponent == QStringLiteral(".")) { 0042 continue; 0043 } 0044 if(lPathComponent == QStringLiteral("..")) { 0045 lNode = qobject_cast<Node *>(lNode->parent()); 0046 } else { 0047 auto lDir = qobject_cast<Directory *>(lNode); 0048 if(lDir == nullptr) { 0049 return nullptr; 0050 } 0051 lNode = lDir->subNodes().value(lPathComponent, nullptr); 0052 } 0053 if(lNode == nullptr) { 0054 return nullptr; 0055 } 0056 } 0057 if(pFollowLinks && !lNode->mSymlinkTarget.isEmpty()) { 0058 return qobject_cast<Node *>(lNode->parent())->resolve(lNode->mSymlinkTarget, true); 0059 } 0060 return lNode; 0061 } 0062 0063 QString Node::completePath() { 0064 QString lCompletePath; 0065 Node *lNode = this; 0066 while(lNode != nullptr) { 0067 Node *lNewNode = qobject_cast<Node *>(lNode->parent()); 0068 if(lNewNode == nullptr) { //this must be the repository, already starts and ends with slash. 0069 QString lObjectName = lNode->objectName(); 0070 lObjectName.chop(1); 0071 lCompletePath.prepend(lObjectName); 0072 } else { 0073 lCompletePath.prepend(lNode->objectName()); 0074 lCompletePath.prepend(QStringLiteral("/")); 0075 } 0076 lNode = lNewNode; 0077 } 0078 return lCompletePath; 0079 } 0080 0081 Node *Node::parentCommit() { 0082 Node *lNode = this; 0083 while(lNode != nullptr && qobject_cast<Branch *>(lNode->parent()) == nullptr) { 0084 lNode = qobject_cast<Node *>(lNode->parent()); 0085 } 0086 return lNode; 0087 } 0088 0089 //Node *Node::parentRepository() { 0090 // Node *lNode = this; 0091 // while(lNode->parent() != nullptr && qobject_cast<Repository *>(lNode) == nullptr) { 0092 // lNode = qobject_cast<Node *>(lNode->parent()); 0093 // } 0094 // return lNode; 0095 //} 0096 0097 Directory::Directory(QObject *pParent, const QString &pName, qint64 pMode) 0098 :Node(pParent, pName, pMode) 0099 { 0100 mSubNodes = nullptr; 0101 mMimeType = QStringLiteral("inode/directory"); 0102 } 0103 0104 NodeMap Directory::subNodes() { 0105 if(mSubNodes == nullptr) { 0106 mSubNodes = new NodeMap(); 0107 generateSubNodes(); 0108 } 0109 return *mSubNodes; 0110 } 0111 0112 int File::readMetadata(VintStream &pMetadataStream) { 0113 int lRetVal = Node::readMetadata(pMetadataStream); 0114 QByteArray lContent, lNextData; 0115 seek(0); 0116 while(lContent.size() < 1000 && 0 == read(lNextData)) { 0117 lContent.append(lNextData); 0118 } 0119 seek(0); 0120 QMimeDatabase db; 0121 if(!lContent.isEmpty()) { 0122 mMimeType = db.mimeTypeForFileNameAndData(objectName(), lContent).name(); 0123 } else { 0124 mMimeType = db.mimeTypeForFile(objectName()).name(); 0125 } 0126 return lRetVal; 0127 } 0128 0129 BlobFile::BlobFile(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode) 0130 : File(pParent, pName, pMode), mOid(*pOid), mBlob(nullptr) 0131 {} 0132 0133 BlobFile::~BlobFile() { 0134 git_blob_free(mBlob); 0135 } 0136 0137 int BlobFile::read(QByteArray &pChunk, qint64 pReadSize) { 0138 if(mOffset >= size()) { 0139 return KIO::ERR_NO_CONTENT; 0140 } 0141 git_blob *lBlob = cachedBlob(); 0142 if(lBlob == nullptr) { 0143 return KIO::ERR_CANNOT_READ; 0144 } 0145 quint64 lAvailableSize = size() - mOffset; 0146 quint64 lReadSize = lAvailableSize; 0147 if(pReadSize > 0 && static_cast<quint64>(pReadSize) < lAvailableSize) { 0148 lReadSize = static_cast<quint64>(pReadSize); 0149 } 0150 pChunk = QByteArray::fromRawData(static_cast<const char *>(git_blob_rawcontent(lBlob)) + mOffset, static_cast<int>(lReadSize)); 0151 mOffset += lReadSize; 0152 return 0; 0153 } 0154 0155 git_blob *BlobFile::cachedBlob() { 0156 if(mBlob == nullptr) { 0157 git_blob_lookup(&mBlob, mRepository, &mOid); 0158 } 0159 return mBlob; 0160 } 0161 0162 quint64 BlobFile::calculateSize() { 0163 if(mSize >= 0) { 0164 return static_cast<quint64>(mSize); 0165 } 0166 git_blob *lBlob = cachedBlob(); 0167 if(lBlob == nullptr) { 0168 return 0; 0169 } 0170 return static_cast<quint64>(git_blob_rawsize(lBlob)); 0171 } 0172 0173 ChunkFile::ChunkFile(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode) 0174 : File(pParent, pName, pMode), mOid(*pOid), mCurrentBlob(nullptr), mValidSeekPosition(false) 0175 { 0176 ChunkFile::seek(0); 0177 } 0178 0179 ChunkFile::~ChunkFile() { 0180 if(mCurrentBlob != nullptr) { 0181 git_blob_free(mCurrentBlob); 0182 } 0183 } 0184 0185 int ChunkFile::seek(quint64 pOffset) { 0186 if(pOffset >= size()) { 0187 return KIO::ERR_CANNOT_SEEK; 0188 } 0189 if(mOffset == pOffset && mValidSeekPosition) { 0190 return 0; // nothing to do, success 0191 } 0192 0193 mOffset = pOffset; 0194 mValidSeekPosition = false; 0195 0196 while(!mPositionStack.isEmpty()) { 0197 delete mPositionStack.takeLast(); 0198 } 0199 if(mCurrentBlob != nullptr) { 0200 git_blob_free(mCurrentBlob); 0201 mCurrentBlob = nullptr; 0202 } 0203 0204 git_tree *lTree; 0205 if(0 != git_tree_lookup(&lTree, mRepository, &mOid)) { 0206 return KIO::ERR_CANNOT_SEEK; 0207 } 0208 0209 auto lCurrentPos = new TreePosition(lTree); 0210 mPositionStack.append(lCurrentPos); 0211 quint64 lLocalOffset = mOffset; 0212 while(true) { 0213 ulong lLower = 0; 0214 const git_tree_entry *lLowerEntry = git_tree_entry_byindex(lCurrentPos->mTree, lLower); 0215 ulong lLowerOffset = 0; 0216 ulong lUpper = git_tree_entrycount(lCurrentPos->mTree); 0217 0218 while(lUpper - lLower > 1) { 0219 ulong lToCheck = lLower + (lUpper - lLower)/2; 0220 const git_tree_entry *lCheckEntry = git_tree_entry_byindex(lCurrentPos->mTree, lToCheck); 0221 quint64 lCheckOffset; 0222 if(!offsetFromName(lCheckEntry, lCheckOffset)) { 0223 return KIO::ERR_CANNOT_SEEK; 0224 } 0225 if(lCheckOffset > lLocalOffset) { 0226 lUpper = lToCheck; 0227 } else { 0228 lLower = lToCheck; 0229 lLowerEntry = lCheckEntry; 0230 lLowerOffset = lCheckOffset; 0231 } 0232 } 0233 lCurrentPos->mIndex = lLower; 0234 // the remainder of the offset will be a local offset into the blob or into the subtree. 0235 lLocalOffset -= lLowerOffset; 0236 0237 if(S_ISDIR(git_tree_entry_filemode(lLowerEntry))) { 0238 git_tree *lLowerTree; 0239 if(0 != git_tree_lookup(&lLowerTree, mRepository, git_tree_entry_id(lLowerEntry))) { 0240 return KIO::ERR_CANNOT_SEEK; 0241 } 0242 lCurrentPos = new TreePosition(lLowerTree); 0243 mPositionStack.append(lCurrentPos); 0244 } else { 0245 lCurrentPos->mSkipSize = lLocalOffset; 0246 break; 0247 } 0248 } 0249 mValidSeekPosition = true; 0250 return 0; // success. 0251 } 0252 0253 int ChunkFile::read(QByteArray &pChunk, qint64 pReadSize) { 0254 if(mOffset >= size()) { 0255 return KIO::ERR_NO_CONTENT; 0256 } 0257 if(!mValidSeekPosition) { 0258 return KIO::ERR_CANNOT_READ; 0259 } 0260 0261 TreePosition *lCurrentPos = mPositionStack.last(); 0262 if(mCurrentBlob != nullptr && lCurrentPos->mSkipSize == 0) { 0263 // skipsize has been reset, this means current blob has been exhausted. Free it 0264 // now as we're about to fetch a new one. 0265 git_blob_free(mCurrentBlob); 0266 mCurrentBlob = nullptr; 0267 } 0268 0269 if(mCurrentBlob == nullptr) { 0270 const git_tree_entry *lTreeEntry = git_tree_entry_byindex(lCurrentPos->mTree, lCurrentPos->mIndex); 0271 if(0 != git_blob_lookup(&mCurrentBlob, mRepository, git_tree_entry_id(lTreeEntry))) { 0272 return KIO::ERR_CANNOT_READ; 0273 } 0274 } 0275 0276 auto lTotalSize = static_cast<quint64>(git_blob_rawsize(mCurrentBlob)); 0277 if(lTotalSize < lCurrentPos->mSkipSize) { // this must mean a corrupt bup tree somehow 0278 return KIO::ERR_CANNOT_READ; 0279 } 0280 quint64 lAvailableSize = lTotalSize - lCurrentPos->mSkipSize; 0281 quint64 lReadSize = lAvailableSize; 0282 if(pReadSize > 0 && static_cast<quint64>(pReadSize) < lAvailableSize) { 0283 lReadSize = static_cast<quint64>(pReadSize); 0284 } 0285 pChunk = QByteArray::fromRawData(static_cast<const char *>(git_blob_rawcontent(mCurrentBlob)) + lCurrentPos->mSkipSize, static_cast<int>(lReadSize)); 0286 mOffset += lReadSize; 0287 lCurrentPos->mSkipSize += lReadSize; 0288 0289 // check if it's time to find next blob. 0290 if(lCurrentPos->mSkipSize == lTotalSize) { 0291 lCurrentPos->mSkipSize = 0; 0292 lCurrentPos->mIndex++; 0293 while(true) { 0294 if(lCurrentPos->mIndex < git_tree_entrycount(lCurrentPos->mTree)) { 0295 const git_tree_entry *lTreeEntry = git_tree_entry_byindex(lCurrentPos->mTree, lCurrentPos->mIndex); 0296 if(S_ISDIR(git_tree_entry_filemode(lTreeEntry))) { 0297 git_tree *lTree; 0298 if(0 != git_tree_lookup(&lTree, mRepository, git_tree_entry_id(lTreeEntry))) { 0299 return KIO::ERR_CANNOT_READ; 0300 } 0301 lCurrentPos = new TreePosition(lTree); // will have index and skipsize initialized to zero. 0302 mPositionStack.append(lCurrentPos); 0303 } else { 0304 // it's a blob 0305 break; 0306 } 0307 } else { 0308 delete mPositionStack.takeLast(); 0309 if(mPositionStack.isEmpty()) { 0310 Q_ASSERT(mOffset == size()); 0311 break; 0312 } 0313 lCurrentPos = mPositionStack.last(); 0314 lCurrentPos->mIndex++; 0315 } 0316 } 0317 } 0318 return 0; // success. 0319 } 0320 0321 quint64 ChunkFile::calculateSize() { 0322 if(mSize >= 0) { 0323 return static_cast<quint64>(mSize); 0324 } 0325 return calculateChunkFileSize(&mOid, mRepository); 0326 } 0327 0328 ChunkFile::TreePosition::TreePosition(git_tree *pTree) { 0329 mTree = pTree; 0330 mIndex = 0; 0331 mSkipSize = 0; 0332 } 0333 0334 ChunkFile::TreePosition::~TreePosition() { 0335 git_tree_free(mTree); 0336 } 0337 0338 ArchivedDirectory::ArchivedDirectory(Node *pParent, const git_oid *pOid, const QString &pName, qint64 pMode) 0339 : Directory(pParent, pName, pMode), 0340 mOid(*pOid), 0341 mTree(nullptr), 0342 mMetadataStream(nullptr) 0343 { 0344 if(0 != git_tree_lookup(&mTree, mRepository, &mOid)) { 0345 return; 0346 } 0347 const git_tree_entry *lTreeEntry = git_tree_entry_byname(mTree, ".bupm"); 0348 if(lTreeEntry != nullptr && 0 == git_blob_lookup(&mMetadataBlob, mRepository, git_tree_entry_id(lTreeEntry))) { 0349 mMetadataStream = new VintStream(git_blob_rawcontent(mMetadataBlob), static_cast<int>(git_blob_rawsize(mMetadataBlob)), this); 0350 readMetadata(*mMetadataStream); // the first entry is metadata for the directory itself 0351 } 0352 } 0353 0354 void ArchivedDirectory::generateSubNodes() { 0355 if(mTree == nullptr) { 0356 return; 0357 } 0358 ulong lEntryCount = git_tree_entrycount(mTree); 0359 for(uint i = 0; i < lEntryCount; ++i) { 0360 uint lMode; 0361 const git_oid *lOid; 0362 QString lName; 0363 bool lChunked; 0364 const git_tree_entry *lTreeEntry = git_tree_entry_byindex(mTree, i); 0365 getEntryAttributes(lTreeEntry, lMode, lChunked, lOid, lName); 0366 if(lName == QStringLiteral(".bupm")) { 0367 continue; 0368 } 0369 0370 Node *lSubNode = nullptr; 0371 if(S_ISDIR(lMode)) { 0372 lSubNode = new ArchivedDirectory(this, lOid, lName, lMode); 0373 } else if(S_ISLNK(lMode)) { 0374 lSubNode = new Symlink(this, lOid, lName, lMode); 0375 } else if(lChunked) { 0376 lSubNode = new ChunkFile(this, lOid, lName, lMode); 0377 } else { 0378 lSubNode = new BlobFile(this, lOid, lName, lMode); 0379 } 0380 mSubNodes->insert(lName, lSubNode); 0381 if(!S_ISDIR(lMode) && mMetadataStream != nullptr) { 0382 lSubNode->readMetadata(*mMetadataStream); 0383 } 0384 } 0385 if(mMetadataStream != nullptr) { 0386 delete mMetadataStream; 0387 mMetadataStream = nullptr; 0388 git_blob_free(mMetadataBlob); 0389 mMetadataBlob = nullptr; 0390 } 0391 git_tree_free(mTree); 0392 mTree = nullptr; 0393 } 0394 0395 Branch::Branch(Node *pParent, const char *pName) 0396 : Directory(pParent, QString::fromLocal8Bit(pName).remove(0, 11), DEFAULT_MODE_DIRECTORY), 0397 mRefName(pName) 0398 { 0399 QByteArray lPath = parent()->objectName().toLocal8Bit(); 0400 lPath.append(mRefName); 0401 struct stat lStat; 0402 if(0 == stat(lPath, &lStat)) { 0403 mAtime = lStat.st_atime; 0404 mMtime = lStat.st_mtime; 0405 } 0406 } 0407 0408 void Branch::reload() { 0409 if(mSubNodes == nullptr) { 0410 mSubNodes = new NodeMap(); 0411 } 0412 // potentially changed content in a branch, generateSubNodes is written so 0413 // that it can be called repeatedly. 0414 generateSubNodes(); 0415 } 0416 0417 void Branch::generateSubNodes() { 0418 if(0 != git_revwalk_push_ref(mRevisionWalker, mRefName)) { 0419 return; 0420 } 0421 git_oid lOid; 0422 while(0 == git_revwalk_next(&lOid, mRevisionWalker)) { 0423 git_commit *lCommit; 0424 if(0 != git_commit_lookup(&lCommit, mRepository, &lOid)) { 0425 continue; 0426 } 0427 QString lCommitTimeLocal = vfsTimeToString(git_commit_time(lCommit)); 0428 if(!mSubNodes->contains(lCommitTimeLocal)) { 0429 Directory * lDirectory = new ArchivedDirectory(this, git_commit_tree_id(lCommit), 0430 lCommitTimeLocal, DEFAULT_MODE_DIRECTORY); 0431 lDirectory->mMtime = git_commit_time(lCommit); 0432 mSubNodes->insert(lCommitTimeLocal, lDirectory); 0433 } 0434 git_commit_free(lCommit); 0435 } 0436 } 0437 0438 Repository::Repository(QObject *pParent, const QString &pRepositoryPath) 0439 : Directory(pParent, pRepositoryPath, DEFAULT_MODE_DIRECTORY) 0440 { 0441 if(!objectName().endsWith(QLatin1Char('/'))) { 0442 setObjectName(objectName() + QLatin1Char('/')); 0443 } 0444 if(0 != git_repository_open(&mRepository, pRepositoryPath.toLocal8Bit())) { 0445 qCWarning(KUPKIO) << "could not open repository " << pRepositoryPath; 0446 mRepository = nullptr; 0447 return; 0448 } 0449 git_strarray lBranchNames; 0450 git_reference_list(&lBranchNames, mRepository); 0451 for(uint i = 0; i < lBranchNames.count; ++i) { 0452 QString lRefName = QString::fromLocal8Bit(lBranchNames.strings[i]); 0453 if(lRefName.startsWith(QStringLiteral("refs/heads/"))) { 0454 QString lPath = objectName(); 0455 lPath.append(lRefName); 0456 struct stat lStat; 0457 stat(lPath.toLocal8Bit(), &lStat); 0458 if(lStat.st_atime > mAtime) { 0459 mAtime = lStat.st_atime; 0460 } 0461 if(lStat.st_mtime > mMtime) { 0462 mMtime = lStat.st_mtime; 0463 } 0464 } 0465 } 0466 git_strarray_free(&lBranchNames); 0467 0468 if(0 != git_revwalk_new(&mRevisionWalker, mRepository)) { 0469 qCWarning(KUPKIO) << "could not create a revision walker in repository " << pRepositoryPath; 0470 mRevisionWalker = nullptr; 0471 return; 0472 } 0473 } 0474 0475 Repository::~Repository() { 0476 if(mRepository != nullptr) { 0477 git_repository_free(mRepository); 0478 } 0479 if(mRevisionWalker != nullptr) { 0480 git_revwalk_free(mRevisionWalker); 0481 } 0482 } 0483 0484 void Repository::generateSubNodes() { 0485 git_strarray lBranchNames; 0486 git_reference_list(&lBranchNames, mRepository); 0487 for(uint i = 0; i < lBranchNames.count; ++i) { 0488 auto lRefName = QString::fromLocal8Bit(lBranchNames.strings[i]); 0489 if(lRefName.startsWith(QStringLiteral("refs/heads/"))) { 0490 auto lBranch = new Branch(this, lBranchNames.strings[i]); 0491 mSubNodes->insert(lBranch->objectName(), lBranch); 0492 } 0493 } 0494 git_strarray_free(&lBranchNames); 0495 } 0496 0497