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