/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* Copyright 2013-2021 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt SPDX-License-Identifier: BSD-2-Clause-FreeBSD SPDX-FileCopyrightText: 2021 The Libfossil Authors SPDX-ArtifactOfProjectName: Libfossil SPDX-FileType: Code Heavily indebted to the Fossil SCM project (https://fossil-scm.org). */ /************************************************************************ This file contains some of the APIs dealing with the checkout state. */ #include "fossil-scm/internal.h" #include "fossil-scm/hash.h" #include "fossil-scm/checkout.h" #include "fossil-scm/confdb.h" #include /* Only for debugging */ #include #define MARKER(pfexp) \ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ printf pfexp; \ } while(0) int fsl_vfile_load(fsl_cx * const f, fsl_id_t vid, bool clearOtherVersions, uint32_t * missingCount){ fsl_db * dbC = f ? fsl_needs_ckout(f) : NULL; fsl_db * dbR = dbC ? fsl_needs_repo(f) : NULL; fsl_deck d = fsl_deck_empty; fsl_stmt qIns = fsl_stmt_empty; fsl_stmt qRid = fsl_stmt_empty; int rc; bool alreadyHad; fsl_card_F const * fc; assert(dbC && "Must only be called when a checkout is opened."); assert(dbR && "Must only be called when a repo is opened."); if(!dbC) return FSL_RC_NOT_A_CKOUT; else if(!dbR) return FSL_RC_NOT_A_REPO; if(vid<=0) vid = f->ckout.rid; assert(vid>=0); rc = fsl_cx_transaction_begin(f); if(rc) return rc; alreadyHad = fsl_db_exists(dbC, "SELECT 1 FROM vfile " "WHERE vid=%" FSL_ID_T_PFMT, vid); if(clearOtherVersions){ /* Reminder to self: DO NOT clear vmerge here. Doing so will break merge tracking in the checkin process. */ rc = fsl_vfile_unload_except(f, vid); if(rc) goto end; } if(alreadyHad){ /* Already done. */ rc = 0; goto end; } assert(0==rc); if(0==vid){ /* This is either misuse or an empty/initial repo with no checkins. Let's assume the latter, since that's what triggered the addition of this check. */ goto end; } rc = fsl_deck_load_rid(f, &d, vid, FSL_SATYPE_CHECKIN); if(rc) goto end; assert(d.rid==vid); rc = fsl_deck_F_rewind(&d); if(rc) goto end; rc = fsl_db_prepare(dbC, &qIns, "INSERT INTO vfile" "(vid,isexe,islink,rid,mrid,pathname,mhash) " "VALUES(:vid,:isexe,:islink,:id,:id,:name,null)" /* 2021-10-15: why do we not set mtime here? */); if(rc) goto end; rc = fsl_db_prepare(dbR, &qRid, "SELECT rid,size FROM blob WHERE uuid=?"); if(rc) goto end; rc = fsl_stmt_bind_id_name(&qIns, ":vid", vid); while( !rc && !(rc=fsl_deck_F_next(&d, &fc)) && fc){ fsl_id_t rid; int64_t size; assert(fc->uuid && "We should never see F-card deletions " "via fsl_deck_F_next()"); if(fsl_uuid_is_shunned(f,fc->uuid)) continue; rc = fsl_stmt_bind_text(&qRid, 1, fc->uuid, -1, 0); if(rc) break; rc = fsl_stmt_step(&qRid); if(FSL_RC_STEP_ROW==rc){ rid = fsl_stmt_g_id(&qRid,0); size = fsl_stmt_g_int64(&qRid,1); }else if(FSL_RC_STEP_DONE==rc){ rid = 0; size = 0; }else{ assert(qRid.db->error.code); rc = fsl_cx_uplift_db_error(f, qRid.db); break; } fsl_stmt_reset(&qRid); if( !rid || size<0 ){ if(missingCount) ++*missingCount; continue; } fsl_stmt_bind_int32_name(&qIns, ":isexe", (FSL_FILE_PERM_EXE & fc->perm) ? 1 : 0); fsl_stmt_bind_int32_name(&qIns, ":islink", (FSL_FILE_PERM_LINK & fc->perm) ? 1 : 0); fsl_stmt_bind_id_name(&qIns, ":id", rid); rc = fsl_stmt_bind_text_name(&qIns, ":name", fc->name, -1, 0); if(rc) break; rc = fsl_stmt_step(&qIns); if(FSL_RC_STEP_DONE!=rc) break; else rc = 0; fsl_stmt_reset(&qIns); } end: fsl_stmt_finalize(&qIns); fsl_stmt_finalize(&qRid); /* Update f->ckout state and some db bits we need when changing the checkout. */ if(!rc && vid>0){ if(!alreadyHad){ assert(d.rid>0); } } fsl_deck_finalize(&d); if(rc) fsl_cx_transaction_end(f, true); else rc = fsl_cx_transaction_end(f, false); if(rc && !f->error.code){ if(dbC->error.code) fsl_cx_uplift_db_error(f, dbC); else if(dbR->error.code) fsl_cx_uplift_db_error(f, dbR); } return rc; } static int fsl_vfile_unload_impl(fsl_cx * const f, fsl_id_t vid, bool oneVersion){ fsl_db * const db = fsl_needs_ckout(f); if(!db) return FSL_RC_NOT_A_CKOUT; if(vid<=0) vid = f->ckout.rid; int const rc = fsl_db_exec(db, "DELETE FROM vfile " "WHERE vid%s%" FSL_ID_T_PFMT " /* %s() */", oneVersion ? "=" : "<>", vid, __func__); return rc ? fsl_cx_uplift_db_error2(f, db, rc) : 0; } int fsl_vfile_unload(fsl_cx * const f, fsl_id_t vid){ return fsl_vfile_unload_impl(f, vid, true); } int fsl_vfile_unload_except(fsl_cx * const f, fsl_id_t vid){ return fsl_vfile_unload_impl(f, vid, false); } /** Internal code de-duplifier for places which need to re-check a file's hash in order to be sure whether it was really modified. hashLen must be the length of the previous (db-side) hash of the file. This routine will hash that file using the same hash type. The new hash is appended to pTgt. Returns 0 on success. */ static int fsl_vfile_recheck_file_hash( fsl_cx * const f, const char * const zName, fsl_size_t hashLen, fsl_buffer * const pTgt ){ bool errReported = false; int rc = 0; if((fsl_size_t)FSL_STRLEN_SHA1==hashLen){ rc = fsl_sha1sum_filename(zName, pTgt); }else if((fsl_size_t)FSL_STRLEN_K256==hashLen){ rc = fsl_sha3sum_filename(zName, pTgt); }else{ assert(!"This \"cannot happen\"."); rc = fsl_cx_err_set(f, FSL_RC_CHECKSUM_MISMATCH, "Cannot determine which hash to use for file: %s", zName); errReported = true; } if(rc && !errReported && FSL_RC_OOM != rc){ rc = fsl_cx_err_set(f, rc, "Error %s while hashing file: %s", fsl_rc_cstr(rc), zName); } return rc; } int fsl_vfile_changes_scan(fsl_cx * const f, fsl_id_t vid, unsigned cksigFlags){ fsl_stmt * stUpdate = NULL; fsl_stmt q = fsl_stmt_empty; int rc = 0; fsl_fstat fst = fsl_fstat_empty; fsl_size_t rootLen; fsl_buffer * fileCksum = fsl__cx_scratchpad(f); bool const useMtime = (cksigFlags & FSL_VFILE_CKSIG_HASH)==0 && fsl_config_get_bool(f, FSL_CONFDB_REPO, true, "mtime-changes"); if(!fsl_needs_ckout(f)) return FSL_RC_NOT_A_CKOUT; assert(f->ckout.dir); if(vid<=0) vid = f->ckout.rid; assert(vid>=0); rootLen = fsl_strlen(f->ckout.dir); assert(rootLen); rc = fsl_cx_transaction_begin(f); if(rc) return rc; if(f->ckout.rid != vid){ rc = fsl_vfile_load(f, vid, (FSL_VFILE_CKSIG_KEEP_OTHERS & cksigFlags) ? false : true, NULL); } if(rc) goto end; #if 0 MARKER(("changed/deleted vfile contents post load-from-rid:\n")); fsl_db_each( fsl_cx_db_ckout(f), fsl_stmt_each_f_dump, NULL, "SELECT vf.id, substr(b.uuid,0,8) hash, chnged, " "deleted, vf.pathname " "FROM vfile vf LEFT JOIN blob b " "ON b.rid=vf.rid " "WHERE vf.vid=%"FSL_ID_T_PFMT" " "AND (chnged<>0 OR pathname<>origname OR deleted<>0)" "ORDER BY vf.id", vid); #endif rc = fsl_cx_prepare(f, &q, "SELECT " /*0*/"id," /*1*/"%Q || pathname," /*2*/"vfile.mrid," /*3*/"deleted," /*4*/"chnged," /*5*/"uuid," /*6*/"size," /*7*/"mtime," /*8*/"isexe," /*9*/"islink, " /*10*/"CASE WHEN isexe THEN %d " "WHEN islink THEN %d ELSE %d END " "FROM vfile LEFT JOIN blob ON vfile.mrid=blob.rid " "WHERE vid=%"FSL_ID_T_PFMT, f->ckout.dir, FSL_FILE_PERM_EXE, FSL_FILE_PERM_LINK, FSL_FILE_PERM_REGULAR, (fsl_id_t)vid); if(rc) goto end; while( fsl_stmt_step(&q) == FSL_RC_STEP_ROW ){ fsl_id_t id, rid; char const * zName; #ifndef _WIN32 //char const * relName; #endif fsl_size_t nName = 0; int isDeleted; int64_t currentSize; int64_t origSize; int changed, oldChanged; //int isExe; fsl_time_t oldMtime, currentMtime; #if !defined(_WIN32) int origPerm; int currentPerm; #endif id = fsl_stmt_g_id(&q, 0); assert(id>0); zName = fsl_stmt_g_text(&q, 1, &nName); rid = fsl_stmt_g_id(&q, 2); isDeleted = fsl_stmt_g_int32(&q, 3); oldChanged = changed = fsl_stmt_g_int32(&q, 4); origSize = fsl_stmt_g_int64(&q, 6); oldMtime = (fsl_time_t)fsl_stmt_g_int64(&q, 7); //isExe = fsl_stmt_g_int32(&q, 8); rc = fsl_cx_stat( f, false, zName, &fst ); currentSize = rc ? -1 : (int64_t)fst.size; currentMtime = rc ? 0 : fst.mtime; if(rc){ fsl_cx_err_reset(f); rc = 0; } #if !defined(_WIN32) //relName = zName + rootLen; origPerm = fsl_stmt_g_int32(&q, 10); currentPerm = (FSL_FSTAT_PERM_EXE==fst.perm ? FSL_FILE_PERM_EXE : FSL_FILE_PERM_REGULAR) /*(FSL_FSTAT_TYPE_LINK==fst.type ? FSL_FILE_PERM_LINK : FSL_FILE_PERM_REGULAR)*/ /* ^^^ FIXME: this isn't right for symlinks. For those we have to treat them as FSL_FILE_PERM_LINK when the repo has symlink support enabled, else FSL_FILE_PERM_REGULAR. That's to fix if/when we ever support SCM'd symlinks in the library. */ ; #endif if(!changed && (isDeleted || !rid)){ /* ADD and REMOVE operations always change the file */ changed = FSL_VFILE_CHANGE_MOD; } else if( currentSize>=0 && !(FSL_FSTAT_TYPE_FILE==fst.type || FSL_FSTAT_TYPE_LINK==fst.type)){ if( FSL_VFILE_CKSIG_ENOTFILE & cksigFlags ){ rc = fsl_cx_err_set(f, FSL_RC_TYPE, "Not an ordinary file or symlink: %s", zName); goto end; } changed = FSL_VFILE_CHANGE_MOD; } if(origSize!=currentSize){ changed = FSL_VFILE_CHANGE_MOD; /* A file size change is definitive - the file has changed. No need to check the mtime or hash */ }else if( changed==FSL_VFILE_CHANGE_MOD && rid!=0 && !isDeleted ){ /* File is believed to have changed but it is the same size. Double check that it really has changed by looking at its content. */ fsl_size_t nUuid = 0; char const * uuid; fsl_buffer_reuse(fileCksum); assert( origSize==currentSize ); uuid = fsl_stmt_g_text(&q, 5, &nUuid); assert(uuid && fsl_is_uuid_len((int)nUuid)); rc = fsl_vfile_recheck_file_hash(f, zName, (int)nUuid, fileCksum); if(rc) goto end; assert(fsl_is_uuid_len((int)fileCksum->used)); if( 0 == fsl_uuidcmp(fsl_buffer_cstr(fileCksum), uuid) ){ changed = 0; } }else if( (changed==FSL_VFILE_CHANGE_NONE || changed==FSL_VFILE_CHANGE_MERGE_MOD || changed==FSL_VFILE_CHANGE_INTEGRATE_MOD) && (!useMtime || currentMtime!=oldMtime) ){ /* For files that were formerly believed to be unchanged or that were changed by merging, if their mtime changes, or unconditionally if FSL_VFILE_CKSIG_SETMTIME is used, check to see if they have been edited by looking at their hash sum */ fsl_size_t nUuid = 0; char const * uuid; assert( origSize==currentSize ); uuid = fsl_stmt_g_text(&q, 5, &nUuid); assert(uuid && fsl_is_uuid_len((int)nUuid)); fsl_buffer_reuse(fileCksum); rc = fsl_vfile_recheck_file_hash(f, zName, nUuid, fileCksum); if(rc) goto end; assert(fsl_is_uuid_len((int)fileCksum->used)); if( fsl_uuidcmp(fsl_buffer_cstr(fileCksum), uuid) ){ changed = FSL_VFILE_CHANGE_MOD; } /* MARKER(("SHA compare says %d: %s\n", changed, zName)); */ } if( (cksigFlags & FSL_VFILE_CKSIG_SETMTIME) && (changed==FSL_VFILE_CHANGE_NONE || changed==FSL_VFILE_CHANGE_MERGE_MOD || changed==FSL_VFILE_CHANGE_INTEGRATE_MOD) ){ fsl_time_t desiredMtime = 0; if( 0==fsl_mtime_of_manifest_file(f, vid, rid, &desiredMtime)){ if( currentMtime != desiredMtime ){ fsl_file_mtime_set(zName, desiredMtime); currentMtime = fsl_file_mtime(zName); } } } /* Check for perms differences. */ #if !defined(_WIN32) if( origPerm!=FSL_FILE_PERM_LINK && currentPerm==FSL_FILE_PERM_LINK ){ /* Changing to a symlink takes priority over all other change types. */ changed = FSL_VFILE_CHANGE_BECAME_SYMLINK; }else if( changed==0 || changed==FSL_VFILE_CHANGE_IS_EXEC || changed==FSL_VFILE_CHANGE_BECAME_SYMLINK || changed==FSL_VFILE_CHANGE_NOT_EXEC || changed==FSL_VFILE_CHANGE_NOT_SYMLINK ){ /* Confirm metadata change types. */ if( origPerm==currentPerm ){ changed = 0; }else if( currentPerm==FSL_FILE_PERM_EXE ){ changed = FSL_VFILE_CHANGE_IS_EXEC; }else if( origPerm==FSL_FILE_PERM_EXE ){ changed = FSL_VFILE_CHANGE_NOT_EXEC; }else if( origPerm==FSL_FILE_PERM_LINK ){ changed = FSL_VFILE_CHANGE_NOT_SYMLINK; } } #endif if( currentMtime!=oldMtime || changed!=oldChanged ){ if(!stUpdate){ rc = fsl_cx_prepare_cached(f, &stUpdate, "UPDATE vfile SET " "mtime=?1, chnged=?2 " "WHERE id=?3 " "/*%s()*/",__func__); if(rc) goto end; } rc = fsl_stmt_bind_step(stUpdate, "IiR", currentMtime, changed, id); if(rc) goto end; /* MARKER(("UPDATED vfile.(mtime,chnged) for: %s\n", zName)); */ } }/*while(step)*/ #if 0 MARKER(("changed/deleted vfile contents post vfile scan:\n")); fsl_db_each( fsl_cx_db_ckout(f), fsl_stmt_each_f_dump, NULL, "SELECT vf.id, substr(b.uuid,0,8) hash, chnged, " "deleted, vf.pathname " "FROM vfile vf LEFT JOIN blob b " "ON b.rid=vf.rid " "WHERE vf.vid=%"FSL_ID_T_PFMT" " "AND (chnged<>0 OR pathname<>origname OR deleted<>0)" "ORDER BY vf.id", vid); #endif end: fsl__cx_scratchpad_yield(f, fileCksum); if(!rc){ rc = fsl__ckout_clear_merge_state(f, false); } if(!rc && (cksigFlags & FSL_VFILE_CKSIG_WRITE_CKOUT_VERSION) && (f->ckout.rid != vid)){ rc = fsl__ckout_version_write(f, vid, 0); } if(rc) { fsl_cx_transaction_end(f, true); }else{ rc = fsl_cx_transaction_end(f, false); } fsl_stmt_cached_yield(stUpdate); fsl_stmt_finalize(&q); return rc; } int fsl__vfile_to_ckout(fsl_cx * const f, fsl_id_t vfileId, int * wasWritten){ int rc = 0; fsl_db * const db = fsl_needs_ckout(f); fsl_stmt q = fsl_stmt_empty; int counter = 0; fsl_buffer content = fsl_buffer_empty; char const * sql; fsl_id_t qArg; fsl_fstat * const fst = &f->cache.fstat; if(!db) return FSL_RC_NOT_A_CKOUT; assert(f->ckout.rid); if(vfileId){ sql = "SELECT v.id, " "%Q || v.pathname, " "v.mrid, " "v.isexe, v.islink, b.uuid, b.size " "FROM vfile v, blob b" " WHERE v.id=%" FSL_ID_T_PFMT " AND v.mrid>0 " " AND v.mrid=b.rid /*%s()*/"; qArg = vfileId; }else{ sql = "SELECT v.id, " "%Q || v.pathname, " "v.mrid, " "v.isexe, v.islink, b.uuid, b.size " "FROM vfile v, blob b" " WHERE v.vid=%" FSL_ID_T_PFMT " AND v.mrid>0 " " AND v.mrid=b.rid /*%s()*/"; qArg = f->ckout.rid; } #undef VFILE_NAMEPART assert(qArg>=0); rc = fsl_db_prepare(db, &q, sql, f->ckout.dir, qArg, __func__); if(rc){ rc = fsl_cx_uplift_db_error2(f, db, rc); goto end; } while(FSL_RC_STEP_ROW==(rc = fsl_stmt_step(&q))){ //fsl_id_t const id = fsl_stmt_g_id(&q, 0); fsl_id_t const rid = fsl_stmt_g_id(&q, 2); int32_t const isExe = fsl_stmt_g_int32(&q, 3); int32_t const isLink = fsl_stmt_g_int32(&q, 4); int64_t const sz = fsl_stmt_g_int64(&q, 6); fsl_size_t nameLen = 0; char const * zName = fsl_stmt_g_text(&q, 1, &nameLen); fsl_size_t hashLen = 0; char const * zHash = fsl_stmt_g_text(&q, 5, &hashLen); char const * zRelName = &zName[f->ckout.dirLen]; int isMod = 0; ++counter; assert(nameLen > f->ckout.dirLen); rc = fsl__ckout_safe_file_check(f, zName); if(rc) break; assert(fsl_is_uuid_len(hashLen)); f->cache.fstat = fsl_fstat_empty; rc = fsl__is_locally_modified(f, zName, sz, zHash, (fsl_int_t)hashLen, isExe ? FSL_FILE_PERM_EXE : (isLink ? FSL_FILE_PERM_LINK : FSL_FILE_PERM_REGULAR), &isMod) /* that updates f->cache.fstat */; if(rc) break; else if(FSL_FSTAT_TYPE_DIR==fst->type){ /* Fossil checks for this but if this happens then we have an invalid vfile entry or someone replaced a file with a dir. */ rc = fsl_cx_err_set(f, FSL_RC_TYPE, "Cannot overwrite a directory: %s", zRelName); break; } else if(!isMod) continue; else if((rc=fsl_mkdir_for_file(zName, true))){ rc = fsl_cx_err_set(f, rc, "mkdir() failed for file: %s", zName); break; } if(FSL__LOCALMOD_LINK & isMod){ assert(((isLink && FSL_FILE_PERM_LINK!=fst->perm) ||(!isLink && FSL_FILE_PERM_LINK==fst->perm)) && "Expected fsl__is_locally_modified() to set this."); rc = fsl_file_unlink(zName); if(rc){ rc = fsl_cx_err_set(f, rc, "Error removing target to replace it: %s", zRelName); break; } } if(isLink || (isMod & (FSL__LOCALMOD_NOTFOUND | FSL__LOCALMOD_LINK | FSL__LOCALMOD_CONTENT))){ /* switched link type, content changed, or was not found in the filesystem. */ rc = fsl_content_get(f, rid, &content); if(rc) break; } if(isLink){ rc = fsl__ckout_symlink_create(f, zName, fsl_buffer_cstr(&content)); if(wasWritten && !rc) *wasWritten = 2; }else if(isMod & (FSL__LOCALMOD_NOTFOUND | FSL__LOCALMOD_CONTENT)){ /* Not found locally or its contents differ. */ rc = fsl_buffer_to_filename(&content, zName); if(rc){ rc = fsl_cx_err_set(f, rc, "Error writing to file: %s", zRelName); }else if(wasWritten){ *wasWritten = 2; } }else if(wasWritten && (isMod & FSL__LOCALMOD_PERM)){ *wasWritten = 1; } if(rc) break; fsl_file_exec_set(zName, !!isExe); fsl_buffer_reuse(&content); }/*step() loop*/ switch(rc){ case FSL_RC_STEP_DONE: if(counter){ rc = 0; }else{ rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND, "No entry found: vfile.id=%" FSL_ID_T_PFMT, vfileId); } break; default: break; } end: fsl_buffer_clear(&content); fsl_stmt_finalize(&q); return rc; } int fsl_vfile_pathname(fsl_cx * const f, fsl_id_t vfid, bool absolute, char **zOut){ assert(f->ckout.dir); fsl_db * const db = fsl_cx_db_ckout(f); assert(db); int const rc = fsl_db_get_text(db, zOut, NULL, "SELECT %Q || pathname FROM vfile " "WHERE id=%"FSL_ID_T_PFMT, absolute ? f->ckout.dir : "", vfid); if(rc) fsl_cx_uplift_db_error(f, db); return rc; } #undef MARKER