/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* ** Copyright (c) 2013 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** This program 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. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** This file houses the manifest/control-artifact-related APIs. */ #include "fossil-scm/fossil-internal.h" #include #include /* memcmp() */ /* Only for debugging */ #define MARKER(pfexp) \ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ printf pfexp; \ } while(0) void fsl_card_F_clean(fsl_card_F *t){ if(t){ fsl_free(t->name); fsl_free(t->uuid); fsl_free(t->priorName); *t = fsl_card_F_empty; } } void fsl_card_F_free(fsl_card_F *t){ if(t){ fsl_card_F_clean(t); fsl_free(t); } } fsl_card_Q * fsl_card_Q_malloc(char type, fsl_uuid_cstr target, fsl_uuid_cstr baseline){ if(!type || !target || !fsl_is_uuid(target) || (baseline && !fsl_is_uuid(baseline))) return NULL; else{ fsl_card_Q * c = (fsl_card_Q*)fsl_malloc(sizeof(fsl_card_Q)); if(c){ int rc; *c = fsl_card_Q_empty; c->type = (type>0) ? 1 : 0; rc = fsl_buffer_reserve(&c->storage, baseline ? (FSL_UUID_STRLEN*2+2) : FSL_UUID_STRLEN+1); if(!rc){ c->target = fsl_buffer_cstr(&c->storage); fsl_buffer_append(&c->storage, target, FSL_UUID_STRLEN); /* Cannot fail - allocation already done. */ if(baseline){ fsl_buffer_append(&c->storage, "\0", 1); fsl_buffer_append(&c->storage, baseline, FSL_UUID_STRLEN); assert((c->target == fsl_buffer_cstr(&c->storage)) && "We seem to have misjudged the allocation size."); c->baseline = c->target + FSL_UUID_STRLEN + 1; assert(!c->baseline[FSL_UUID_STRLEN]); } assert(!c->target[FSL_UUID_STRLEN]); }else{ fsl_card_Q_free(c); c = NULL; } } return c; } } void fsl_card_Q_free( fsl_card_Q * cp ){ if(cp){ fsl_buffer_clear(&cp->storage); *cp = fsl_card_Q_empty; fsl_free(cp); } } fsl_card_J * fsl_card_J_malloc(char isAppend, char const * field, char const * value){ if(!field || !*field) return NULL; else{ fsl_card_J * c = (fsl_card_J*)fsl_malloc(sizeof(fsl_card_J)); if(c){ int rc; fsl_size_t const lF = fsl_strlen(field); fsl_size_t const lV = value ? fsl_strlen(value) : 0; *c = fsl_card_J_empty; c->isAppend = isAppend ? 1 : 0; rc = fsl_buffer_reserve(&c->storage, lF + 1 + (lV ? (lV+1) : 0)); if(!rc){ c->field = fsl_buffer_cstr(&c->storage); fsl_buffer_append(&c->storage, field, lF); if(value){ fsl_buffer_append(&c->storage, "\0", 1); fsl_buffer_append(&c->storage, value, lV); assert((c->field == fsl_buffer_cstr(&c->storage)) && "We seem to have misjudged the allocation size."); c->value = c->field + lF + 1; assert(!c->value[lV]); } assert(!c->field[lF]); }else{ fsl_card_J_free(c); c = NULL; } } return c; } } void fsl_card_J_free( fsl_card_J * cp ){ if(cp){ fsl_buffer_clear(&cp->storage); *cp = fsl_card_J_empty; fsl_free(cp); } } fsl_card_F * fsl_card_F_malloc(char const * name, char const * uuid, int perms, char const * oldName){ fsl_card_F * t; if(!name || !*name) return NULL; else if(uuid && !fsl_is_uuid(uuid)) return NULL; t = (fsl_card_F *)fsl_malloc(sizeof(fsl_card_F)); if(t){ int rc = 0; *t = fsl_card_F_empty; t->perms = perms; t->name = fsl_strdup(name); if(!t->name) rc = FSL_RC_OOM; if(!rc && uuid){ t->uuid = fsl_strdup(uuid); if(!t->uuid) rc = FSL_RC_OOM; } if(!rc && oldName){ t->priorName = fsl_strdup(oldName); if(!t->priorName) rc = FSL_RC_OOM; } if(rc){ fsl_card_F_free(t); t = NULL; } } return t; } /** ** fsl_list_visitor_f() impl which requires that obj be-a (fsl_card_T*), ** which this function passes to fsl_card_T_free(). */ static int fsl_list_v_fsl_card_T_free(void * obj, void * visitorState ){ if(obj) fsl_card_T_free( (fsl_card_T*)obj ); return 0; } static int fsl_list_v_fsl_card_F_free(void * obj, void * visitorState ){ if(obj) fsl_card_F_free( (fsl_card_F*)obj ); return 0; } static int fsl_list_v_fsl_card_Q_free(void * obj, void * visitorState ){ if(obj) fsl_card_Q_free( (fsl_card_Q*)obj ); return 0; } static int fsl_list_v_fsl_card_J_free(void * obj, void * visitorState ){ if(obj) fsl_card_J_free( (fsl_card_J*)obj ); return 0; } fsl_deck * fsl_deck_malloc(){ fsl_deck * rc = (fsl_deck *)fsl_malloc(sizeof(fsl_deck)); if(rc){ *rc = fsl_deck_empty; rc->allocStamp = &fsl_deck_empty; } return rc; } void fsl_deck_init( fsl_cx * f, fsl_deck * cards, fsl_catype_t type ){ void const * allocStamp = cards->allocStamp; *cards = fsl_deck_empty; cards->allocStamp = allocStamp; cards->f = f; cards->type = type; } void fsl_deck_clean(fsl_deck *m){ if(!m) return; #if 1 { fsl_deck * b; b = m->B.baseline; m->B.baseline = NULL; while(b){ fsl_deck * next = b->B.baseline; b->B.baseline = NULL /* prevent more recursion */; fsl_deck_finalize(b); b = next; } } #else if(m->B.baseline){ fsl_deck_finalize(m->B.baseline); m->B.baseline = NULL; } #endif #define CBUF(X) fsl_buffer_clear(&m->X) #define SFREE(X) if(m->X) fsl_free(m->X); m->X = NULL SFREE(uuid); CBUF(A.name); SFREE(A.tgt); SFREE(A.src); SFREE(B.uuid); CBUF(C); SFREE(E.uuid); fsl_list_clear(&m->F.list, fsl_list_v_fsl_card_F_free, NULL); CBUF(L); fsl_list_clear(&m->J, fsl_list_v_fsl_card_J_free, NULL); SFREE(K); fsl_list_visit_free(&m->M, 1); CBUF(N); fsl_list_visit_free(&m->P, 1); fsl_list_clear(&m->Q, fsl_list_v_fsl_card_Q_free, NULL); SFREE(R); fsl_list_clear(&m->T, fsl_list_v_fsl_card_T_free, NULL); CBUF(U); CBUF(W); fsl_error_clean(&m->error); { void const * allocStampKludge = m->allocStamp; *m = fsl_deck_empty; m->allocStamp = allocStampKludge; } #undef CBUF #undef SFREE } void fsl_deck_finalize(fsl_deck *m){ void const * allocStamp; if(!m) return; allocStamp = m->allocStamp; fsl_deck_clean(m); if(allocStamp == &fsl_deck_empty){ fsl_free(m); }else{ m->allocStamp = allocStamp; } } char fsl_card_is_legal( fsl_catype_t t, char card ){ /* Implements this table: http://fossil-scm.org/index.html/doc/trunk/www/fileformat.wiki#summary */ if('Z'==card) return 1; else switch(t){ case FSL_CATYPE_ANY: switch(card){ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'J': case 'K': case 'L': case 'M': case 'N': case 'P': case 'Q': case 'R': case 'T': case 'U': case 'W': return -1; default: return 0; } case FSL_CATYPE_ATTACHMENT: switch(card){ case 'A': case 'D': return 1; case 'C': case 'N': case 'U': return -1; default: return 0; }; case FSL_CATYPE_CLUSTER: return 'M'==card ? 1 : 0; case FSL_CATYPE_CONTROL: switch(card){ case 'D': case 'T': case 'U': return 1; default: return 0; }; case FSL_CATYPE_EVENT: switch(card){ case 'C': case 'D': case 'E': case 'W': return 1; case 'N': case 'P': case 'T': case 'U': return -1; default: return 0; }; case FSL_CATYPE_MANIFEST: switch(card){ case 'C': case 'D': case 'U': return 1; case 'B': case 'F': case 'N': case 'P': case 'Q': case 'R': case 'T': return -1; default: return 0; }; case FSL_CATYPE_TICKET: switch(card){ case 'D': case 'J': case 'K': case 'U': return 1; default: return 0; }; case FSL_CATYPE_WIKI: switch(card){ case 'D': case 'L': case 'U': case 'W': return 1; case 'N': case 'P': return -1; default: return 0; }; default: assert(!"Invalid fsl_catype_t."); return 0; }; } char fsl_deck_has_required_cards( fsl_deck const * d ){ fsl_catype_t t = d ? d->type : FSL_CATYPE_ANY; if(!d) return 0; #define HAS_D (d->D > 0) else switch(t){ case FSL_CATYPE_ANY: return 0; case FSL_CATYPE_ATTACHMENT: return d->A.name.used && d->A.tgt && HAS_D; case FSL_CATYPE_CLUSTER: return d->M.used ? 1 : 0; case FSL_CATYPE_CONTROL: return HAS_D && d->U.used ; case FSL_CATYPE_EVENT: return HAS_D && (d->E.julian>0) && d->E.uuid && d->W.used ; case FSL_CATYPE_MANIFEST: return HAS_D && d->C.used && d->U.used #if 1 /* Manifest #1 has an empty file list and an R-card with a constant (repo/manifest-independent) hash (d41d8cd98f00b204e9800998ecf8427e, the initial MD5 hash state). */ && d->R #else && ((d->F.list.used && d->R) /* We need both or neither of F- and R-cards. */ || (!d->F.list.used && !d->R)) #endif ; case FSL_CATYPE_TICKET: return HAS_D && d->K && d->U.used && d->J.used /* Is a J strictly required? Spec is not clear but DRH confirms the current fossil(1) code expects a J card. */ ; case FSL_CATYPE_WIKI: return HAS_D && d->L.used && d->U.used && d->W.used ; case FSL_CATYPE_INVALID: return 0; default: assert(!"Invalid fsl_catype_t."); return 0; }; #undef HAS_D } char const * fsl_catype_cstr(fsl_catype_t t){ switch(t){ #define C(X) case FSL_CATYPE_##X: return #X C(ANY); C(MANIFEST); C(CLUSTER); C(CONTROL); C(WIKI); C(TICKET); C(ATTACHMENT); C(EVENT); C(INVALID); default: assert(!"UNHANDLED fsl_catype_t"); return "!UNKNOWN!"; } } /** ** If fsl_card_is_legal(d->type, card), returns non-0 (true), ** else updated d->error with a description of the constraint ** violation and returns 0. */ static char fsl_deck_check_type( fsl_deck * d, char card ){ if(fsl_card_is_legal(d->type, card)) return 1; else{ fsl_error_set(&d->error, FSL_RC_TYPE, "Card type '%c' is not allowed " "in control artifacts of type %s.", card, fsl_catype_cstr(d->type)); return 0; } } /* Implements fsl_deck_LETTER_set() for certain letters: those implemented as a fsl_uuid_str or an md5, holding a single hex string value. The function returns FSL_RC_RANGE if (valLen!=ASSERTLEN). ASSERTLEN is assumed to be either an SHA1 or MD5 hash value and it is validated against fsl_validate16(value,valLen), returning FSL_RC_CA_SYNTAX if that check fails. */ static int fsl_deck_h_setexid_impl( fsl_deck * mf, fsl_uuid_cstr value, char letter, fsl_size_t assertLen, char ** mfMember ){ assert(mf); if(!fsl_deck_check_type(mf,letter)) return FSL_RC_TYPE; else if(!value){ fsl_free(*mfMember); *mfMember = NULL; return 0; }else if(fsl_strlen(value) != (assertLen)){ return fsl_error_set(&mf->error, FSL_RC_RANGE, "Invalid length for %c-card: expecting %d.", letter, (int)assertLen); }else if(!fsl_validate16(value, assertLen)) { return fsl_error_set(&mf->error, FSL_RC_CA_SYNTAX, "Invalid hexadecimal value for %c-card.", letter); }else{ if(*mfMember) fsl_free(*mfMember); *mfMember = fsl_strndup( value, assertLen ); return *mfMember ? 0 : FSL_RC_OOM; } } /** ** Implements fsl_set_set_XXX() where XXX is a fsl_buffer member of fsl_deck. */ static int fsl_deck_b_setuffer_impl( fsl_deck * mf, char const * value, fsl_int_t valLen, char letter, fsl_buffer * buf){ assert(mf); if(!fsl_deck_check_type(mf,letter)) return FSL_RC_TYPE; else if(valLen<0) valLen = (fsl_int_t)fsl_strlen(value); buf->used = 0; if(value && (valLen>0)){ return fsl_buffer_append( buf, value, valLen ); }else{ if(buf->mem) buf->mem[0] = 0; return 0; } } int fsl_deck_B_set( fsl_deck * mf, fsl_uuid_cstr uuidBaseline){ return mf ? fsl_deck_h_setexid_impl(mf, uuidBaseline, 'B', FSL_UUID_STRLEN, &mf->B.uuid) : FSL_RC_MISUSE; } int fsl_deck_C_set( fsl_deck * mf, char const * v, fsl_int_t n){ return mf ? fsl_deck_b_setuffer_impl(mf, v, n, 'C', &mf->C) : FSL_RC_MISUSE; } int fsl_deck_J_add( fsl_deck * mf, char isAppend, char const * field, char const * value){ if(!mf || !field) return FSL_RC_MISUSE; else if(!*field) return FSL_RC_RANGE; else if(!fsl_deck_check_type(mf,'J')) return FSL_RC_TYPE; else{ int rc; fsl_card_J * cp = fsl_card_J_malloc(isAppend, field, value); if(!cp) rc = FSL_RC_OOM; else if( 0 != (rc = fsl_list_append(&mf->J, cp))){ fsl_card_J_free(cp); } return rc; } } int fsl_deck_K_set( fsl_deck * mf, fsl_uuid_cstr uuid){ return mf ? fsl_deck_h_setexid_impl(mf, uuid, 'K', FSL_UUID_STRLEN, &mf->K) : FSL_RC_MISUSE; } int fsl_deck_L_set( fsl_deck * mf, char const * v, fsl_int_t n){ return mf ? fsl_deck_b_setuffer_impl(mf, v, n, 'L', &mf->L) : FSL_RC_MISUSE; } int fsl_deck_M_add( fsl_deck * mf, char const *uuid){ int rc; char * dupe; if(!mf || !uuid) return FSL_RC_MISUSE; else if(!fsl_deck_check_type(mf, 'M')) return FSL_RC_TYPE; else if(!fsl_is_uuid(uuid)) return FSL_RC_RANGE; dupe = fsl_strndup(uuid, FSL_UUID_STRLEN); if(!dupe) return FSL_RC_OOM; rc = fsl_list_append( &mf->M, dupe ); if(rc){ fsl_free(dupe); } return rc; } int fsl_deck_N_set( fsl_deck * mf, char const * v, fsl_int_t n){ return mf ? fsl_deck_b_setuffer_impl(mf, v, n, 'N', &mf->N) : FSL_RC_MISUSE; } int fsl_deck_P_add( fsl_deck * mf, char const *parentUuid){ int rc; char * dupe; if(!mf || !parentUuid || !*parentUuid) return FSL_RC_MISUSE; else if(!fsl_deck_check_type(mf, 'P')) return FSL_RC_TYPE; else if(FSL_UUID_STRLEN != fsl_strlen(parentUuid)) return FSL_RC_RANGE; dupe = fsl_strndup(parentUuid, FSL_UUID_STRLEN); if(!dupe) return FSL_RC_OOM; rc = fsl_list_append( &mf->P, dupe ); if(rc){ fsl_free(dupe); } return rc; } fsl_id_t fsl_deck_P_get_id(fsl_deck * d, int index){ if(!d || !d->f) return -1; else if(index>(int)d->P.used) return 0; else return fsl_repo_uuid_to_rid(d->f, (char const *)d->P.list[index]); } int fsl_deck_Q_add( fsl_deck * mf, char type, fsl_uuid_cstr target, fsl_uuid_cstr baseline ){ if(!mf || !target) return FSL_RC_MISUSE; else if(!fsl_deck_check_type(mf,'Q')) return FSL_RC_TYPE; else if(!type || !fsl_is_uuid(target) || (baseline && !fsl_is_uuid(baseline))) return FSL_RC_RANGE; else{ int rc; fsl_card_Q * cp = fsl_card_Q_malloc(type, target, baseline); if(!cp) rc = FSL_RC_OOM; else if( 0 != (rc = fsl_list_append(&mf->Q, cp))){ fsl_card_Q_free(cp); } return rc; } } /** ** A comparison routine for qsort(3) which compares fsl_card_F ** instances in a lexical manner based on their names. The order is ** important for card ordering in generated manifests. */ static int qsort_cmp_F_cards( void const * lhs, void const * rhs ){ fsl_card_F const * l = *((fsl_card_F const **)lhs); fsl_card_F const * r = *((fsl_card_F const **)rhs); /* Compare NULL as larger so that NULLs move to the right. That said, we aren't expecting any NULLs. */ assert(l); assert(r); if(!l) return r ? 1 : 0; else if(!r) return -1; else{ return fsl_strcmp(l->name, r->name); } } static void fsl_deck_F_sort(fsl_deck * mf){ fsl_list_sort(&mf->F.list, qsort_cmp_F_cards ); } int fsl_deck_R_set( fsl_deck * mf, fsl_uuid_cstr md5){ return mf ? fsl_deck_h_setexid_impl(mf, md5, 'R', FSL_MD5_STRLEN, &mf->R) : FSL_RC_MISUSE; } int fsl_deck_R_calc(fsl_deck * mf){ fsl_cx * f = mf ? mf->f : NULL; if(!mf || !f) return FSL_RC_MISUSE; else if(!fsl_deck_check_type(mf,'R')) { assert(mf->error.code); return fsl_cx_err_set_e(f, &mf->error); } else if(!mf->F.list.used){ #if 0 return fsl_cx_err_set(f, FSL_RC_RANGE, "Decks with no F-cards do not " "get an R-card."); #else /* Manifest #1 (first checkin) has an R card with no files. Its hash is the starting state of md5, with no input. */ return (1==mf->rid) ? fsl_deck_R_set(mf, FSL_MD5_INITIAL_HASH) : 0; #endif } else{ int rc = 0; fsl_card_F const * fc; fsl_id_t fileRid; fsl_buffer buf = fsl_buffer_empty; unsigned char digest[16]; char hex[FSL_MD5_STRLEN+1]; fsl_md5_cx md5 = fsl_md5_cx_empty; enum { NumBufSize = 40 }; char numBuf[NumBufSize] = {0}; int i = 0; assert(mf->f); /* memset(numBuf,0,NumBufSize); */ fsl_deck_F_sort(mf); rc = fsl_deck_F_rewind(mf); if(rc) return rc; /* TODO: http://www.fossil-scm.org/fossil/artifact/662553ad434?ln=735-771 For delta manifests, this calculation apparently needs to take into account the F cards from parent manifests. Reminder: the logic for handling an on-disk comparison (for commit purposes) is different. See v1's vfile.c: vfile_aggregate_checksum_xxxx() Missing functionality: - The "wd" (working directory) family of functions, needed for symlink handling. */ #if 0 if(mf->B.uuid){ assert(!"R-card calculation for delta manifests is not yet fully implemented."); return fsl_cx_err_set(f, FSL_RC_NYI, "R-card calculation for delta manifests is " "not yet fully implemented."); } #endif while(1){ rc = fsl_deck_F_next(mf, &fc); /* Need this later for debugging a weird problem with a tcl delta manifest: http://core.tcl.tk/tcl/ Delta manifest #5f37dcc3 while processing file #687 (1-based): FSL_RC_RANGE: "Delta: copy extends past end of input" */ if(0) MARKER(("R rc=%s file #%d: %s\n", fsl_rc_cstr(rc), ++i, fc ? fc->name : "")); if(rc || !fc) break; if(!fc->uuid) continue; fileRid = fsl_repo_uuid_to_rid( f, fc->uuid ); if(fileRid<=0){ assert(f->error.code); rc = f->error.code ? f->error.code : fsl_cx_err_set(f, FSL_RC_ERROR, "Error resolving RID for UUID [%s].", fc->uuid); goto end; } fsl_md5_update_cstr(&md5, fc->name, -1); rc = fsl_content_get(f, fileRid, &buf); if(rc){ goto end; } numBuf[0] = 0; fsl_snprintf(numBuf, NumBufSize, " %"FSL_SIZE_T_PFMT"\n", buf.used); fsl_md5_update_cstr(&md5, numBuf, -1); fsl_md5_update_buffer(&md5, &buf); } if(!rc){ fsl_md5_final(&md5, digest); fsl_md5_digest_to_base16(digest, hex); } end: fsl_buffer_clear(&buf); return rc ? rc : fsl_deck_R_set(mf, hex); } } int fsl_deck_T_add2( fsl_deck * mf, fsl_card_T * t){ return (mf && t) ? (fsl_deck_check_type(mf, 'T') ? fsl_list_append(&mf->T, t) : FSL_RC_TYPE) : FSL_RC_MISUSE; } int fsl_deck_T_add( fsl_deck * mf, fsl_tag_type tagType, char const * uuid, char const * name, char const * value){ if(!mf || !name) return FSL_RC_MISUSE; else if(!fsl_deck_check_type(mf, 'T')) return FSL_RC_TYPE; else if(!*name || (uuid &&!fsl_is_uuid(uuid))) return FSL_RC_RANGE; else switch(tagType){ case FSL_TAGTYPE_CANCEL: case FSL_TAGTYPE_ADD: case FSL_TAGTYPE_PROPAGATING:{ int rc; fsl_card_T * t; t = fsl_card_T_malloc(tagType, uuid, name, value); if(!t) return FSL_RC_OOM; rc = fsl_deck_T_add2( mf, t ); if(rc) fsl_card_T_free(t); return rc; } default: return FSL_RC_RANGE; } } int fsl_deck_U_set( fsl_deck * mf, char const * v, fsl_int_t n){ return mf ? fsl_deck_b_setuffer_impl(mf, v, n, 'U', &mf->U) : FSL_RC_MISUSE; } int fsl_deck_W_set( fsl_deck * mf, char const * v, fsl_int_t n){ return mf ? fsl_deck_b_setuffer_impl(mf, v, n, 'W', &mf->W) : FSL_RC_MISUSE; } int fsl_deck_A_set( fsl_deck * mf, char const * name, char const * tgt, char const * uuidSrc ){ if(!mf || !name || !tgt) return FSL_RC_MISUSE; else if(!fsl_deck_check_type(mf, 'A')) return FSL_RC_TYPE; else if(!*tgt){ return fsl_error_set(&mf->error, FSL_RC_RANGE, "Invalid target name in A card."); } /* TODO: validate tgt based on mf->type and require UUID for types EVENT/TICKET. */ else if(uuidSrc && *uuidSrc && !fsl_is_uuid(uuidSrc)){ return fsl_error_set(&mf->error, FSL_RC_RANGE, "Invalid source UUID in A card."); } else{ int rc = 0; fsl_free(mf->A.tgt); fsl_free(mf->A.src); mf->A.src = mf->A.tgt = NULL; mf->A.tgt = fsl_strndup(tgt,-1); if(!mf->A.tgt) rc = FSL_RC_OOM; else if(uuidSrc && *uuidSrc){ mf->A.src = fsl_strndup(uuidSrc,FSL_UUID_STRLEN); if(!mf->A.src) rc = FSL_RC_OOM /* Leave mf->A.tgt for downstream cleanup. */; } return rc; } } int fsl_deck_D_set( fsl_deck * mf, fsl_double_t date){ if(!mf) return FSL_RC_MISUSE; else if(date<=0) return FSL_RC_RANGE; else if(!fsl_deck_check_type(mf, 'D')) return FSL_RC_TYPE; else{ mf->D = date; return 0; } } int fsl_deck_E_set( fsl_deck * mf, fsl_double_t date, char const * uuid){ if(!mf || !uuid) return FSL_RC_MISUSE; else if(date<=0){ return fsl_error_set(&mf->error, FSL_RC_RANGE, "Invalid date value for E card."); }else if(!fsl_is_uuid(uuid)){ return fsl_error_set(&mf->error, FSL_RC_RANGE, "Invalid UUID in E card."); } else{ mf->E.julian = date; fsl_free(mf->E.uuid); mf->E.uuid = fsl_strndup(uuid, FSL_UUID_STRLEN); return mf->E.uuid ? 0 : FSL_RC_OOM; } } int fsl_deck_F_add2( fsl_deck * mf, fsl_card_F * t){ if(!mf || !t) return FSL_RC_MISUSE; else if(!fsl_deck_check_type(mf, 'F')) return FSL_RC_TYPE; else{ int rc = 0; if(mf->F.list.used == mf->F.list.capacity){ rc = fsl_list_reserve(&mf->F.list, mf->F.list.capacity ? mf->F.list.capacity*2 : 20); } return rc ? rc : fsl_list_append(&mf->F.list, t); } } int fsl_deck_F_add( fsl_deck * mf, char const * name, char const * uuid, fsl_mf_perm_t perms, char const * oldName){ if(!mf || !name) return FSL_RC_MISUSE; else if(!fsl_deck_check_type(mf, 'F')) return FSL_RC_TYPE; else if(!*name){ return fsl_error_set(&mf->error, FSL_RC_RANGE, "F-card name may not be empty."); } else if(!fsl_is_simple_pathname(name, 1) || (oldName && !fsl_is_simple_pathname(oldName, 1))){ return fsl_error_set(&mf->error, FSL_RC_RANGE, "Invalid filename for F-card (simple form required)."); } else if(uuid && !fsl_is_uuid(uuid)){ return fsl_error_set(&mf->error, FSL_RC_RANGE, "Invalid UUID for F-card."); } else { int rc; fsl_card_F * t; switch(perms){ case FSL_MF_PERM_EXE: case FSL_MF_PERM_LINK: case FSL_MF_PERM_REGULAR: break; default: assert(!"Invalid fsl_mf_perm_t value"); return fsl_error_set(&mf->error, FSL_RC_RANGE, "Invalid fsl_mf_perm_t value " "(%d) for file [%s].", perms, name); } t = fsl_card_F_malloc(name, uuid, perms, oldName); if(!t) rc = FSL_RC_OOM; else{ rc = fsl_deck_F_add2( mf, t ); if(rc) fsl_card_F_free(t); } return rc; } } int fsl_deck_F_foreach_own( fsl_deck const * d, fsl_card_F_visitor_f cb, void * visitorState ){ if(!d || !cb) return FSL_RC_MISUSE; else{ fsl_card_F const * fc; fsl_size_t i; int rc = 0; for( i = 0; !rc && (iF.list.used); ++i ){ fc = (fsl_card_F const *)d->F.list.list[i]; rc = cb(fc, visitorState); } return (FSL_RC_BREAK==rc) ? 0 : rc; } } int fsl_deck_F_foreach( fsl_deck * d, char includeBaseline, fsl_card_F_visitor_f cb, void * visitorState ){ if(!d || !cb) return FSL_RC_MISUSE; if(!includeBaseline || !d->B.uuid){ return fsl_deck_F_foreach_own(d, cb, visitorState); }else{ int rc; fsl_card_F const * fc; rc = fsl_deck_F_rewind(d); if(rc) return rc; while( !rc && !(rc=fsl_deck_F_next(d, &fc)) && fc) { rc = cb( fc, visitorState ); } return rc; } } /** ** Output state for fsl_appendf_f_mf() and friends. Used for managing ** the output of a fsl_deck. */ struct fsl_deck_out_state { /** ** Repo db handle. Needed for certain card generation bits. */ fsl_db * db; /** ** The set of cards being output. We use this to delegate certain ** output bits. */ fsl_deck const * cards; /** ** Output routine to send manifest to. */ fsl_output_f out; /** ** State to pass as the first arg of this->out(). */ void * outState; /** ** f() result code, so that we can feed the code back through the ** fsl_appendf() layer. If this is non-0, processing must stop. We ** "could" use this->error.code instead, but this is simple. */ int rc; /** ** Counter for list-visiting routines. Must be re-set before each ** visit loop if the visitor makes use of this (most do not). */ fsl_int_t counter; /** ** Incrementally-calculated MD5 sum of all output sent via ** fsl_appendf_f_mf(). */ fsl_md5_cx md5; /* Holds error state for propagating back to the client. */ fsl_error error; /** ** Scratch buffer for fossilzing bytes and other temporary work. */ fsl_buffer scratch; }; typedef struct fsl_deck_out_state fsl_deck_out_state; static const fsl_deck_out_state fsl_deck_out_state_empty = { NULL/*db*/, NULL/*cards*/, NULL/*out*/, NULL/*fState*/, 0/*rc*/, 0/*counter*/, fsl_md5_cx_empty_m/*md5*/, fsl_error_empty_m/*error*/, fsl_buffer_empty_m/*scratch*/ }; /** ** fsl_appendf_f() impl which forwards its data to arg->out(). arg must ** be a (fsl_deck_out_state *). Updates arg->rc to the result of ** calling arg->out(fp->fState, data, n). If arg->out() succeeds then ** arg->md5 is updated to reflect the given data. */ static fsl_int_t fsl_appendf_f_mf( void * arg, char const * data, fsl_int_t n ){ fsl_deck_out_state * os = (fsl_deck_out_state *)arg; if((n>0) && !(os->rc = os->out(os->outState, data, (fsl_size_t)n)) && (os->md5.isInit)){ fsl_md5_update( &os->md5, data, (fsl_size_t)n ); } return (0==os->rc) ? n : -1; } /** ** Internal helper for fsl_mf_output(). Appends formatted output to ** os->out() via fsl_appendf_f_mf(). Returns os->rc (0 on success). */ static int fsl_deck_append( fsl_deck_out_state * os, char const * fmt, ... ){ fsl_int_t rc; va_list args; assert(os); assert(fmt && *fmt); va_start(args,fmt); rc = fsl_appendfv( fsl_appendf_f_mf, os, fmt, args); va_end(args); if(rc<0 && !os->rc) os->rc = FSL_RC_IO; return os->rc; } /** ** Fossilizes (inp, inp+len] bytes to os->scratch, ** overwriting any existing contents. ** Updates and returns os->rc. */ static int fsl_deck_fossilize( fsl_deck_out_state * os, unsigned char const * inp, fsl_int_t len){ fsl_buffer_reset(&os->scratch); return os->rc = len ? fsl_bytes_fossilize(inp, len, &os->scratch) : 0; } /** ** Fossilizes b->mem to os->scratch. Returns 0 on success and all ** that. */ static int fsl_deck_fossilize_b( fsl_deck_out_state * os, fsl_buffer const * b ){ return b->used ? fsl_deck_fossilize( os, b->mem, (fsl_int_t)b->used) : 0; } /** ** If doFossilize, fossilizes b to os->scratch and passes that result ** to fsl_deck_append(), otherwise uses b's contents directly for ** appending a card (with the given letter) to os: ** ** LETTER CONTENT\\n ** ** This is only useful for cards with simple buffer values. */ static int fsl_deck_out_letter_buf( fsl_deck_out_state * os, char const * letter, fsl_buffer const * b, char doFossilize){ if(b->used){ if(doFossilize){ #if 0 fsl_deck_append(os, "%s %F\n", letter, (char const *)b->mem ); #else /* This is functionally identical but is more efficient b/c we re-use os->scratch. */ fsl_deck_fossilize_b(os, b); if(!os->rc) fsl_deck_append(os, "%s %.*s\n", letter, (int)os->scratch.used, (char const *)os->scratch.mem ); #endif }else{ fsl_deck_append(os, "%s %.*s\n", letter, (int)b->used, (char const *)b->mem ); } } return os->rc; } /* Appends the B card to os from os->cards->B. */ static int fsl_deck_out_B( fsl_deck_out_state * os ){ if(os->cards->B.uuid){ if(!fsl_is_uuid(os->cards->B.uuid)){ os->rc = fsl_error_set(&os->error, FSL_RC_RANGE, "Malformed UUID in B card."); } else{ fsl_deck_append(os, "B %s\n", os->cards->B.uuid); } } return os->rc; } /* Appends the C card to os from os->cards->C. */ static int fsl_deck_out_C( fsl_deck_out_state * os ){ if(os->cards->C.used){ fsl_deck_out_letter_buf(os, "C", &os->cards->C, 1); } return os->rc; } /* Appends the D card to os from os->cards->D. */ static int fsl_deck_out_D( fsl_deck_out_state * os ){ if(os->cards->D > 0.0){ char * ds; ds = fsl_db_julian_to_iso8601(os->db, os->cards->D, 1); if(!ds) os->rc = FSL_RC_OOM; else{ fsl_deck_append(os, "D %s\n", ds) /* Updates os->rc */; fsl_free(ds); } return os->rc; }else{ return 0; } } /* Appends the E card to os from os->cards->E. */ static int fsl_deck_out_E( fsl_deck_out_state * os ){ if(os->cards->E.uuid){ char * ds; char msPrecision = FSL_CATYPE_EVENT!=os->cards->type /* The timestamps on events historically have seconds precision, not ms. */; if(os->cards->E.julian < 0.0){ return os->rc = fsl_error_set(&os->error, FSL_RC_TYPE, "Invalid timestamp on E card"); } ds = fsl_db_julian_to_iso8601(os->db, os->cards->E.julian, msPrecision); if(!ds) os->rc = FSL_RC_OOM; else{ fsl_deck_append(os, "E %s %s\n", ds, os->cards->E.uuid); fsl_free(ds); } return os->rc; }else{ return 0; } } /** ** fsl_list_visitor_f() impl for outputing F cards. obj must ** be a (fsl_card_F *). */ static int fsl_list_v_mf_output_card_F(void * obj, void * visitorState ){ fsl_deck_out_state * os = (fsl_deck_out_state *)visitorState; fsl_card_F const * f = (fsl_card_F const *)obj; int rc; char hasOldName; char const * zPerm; assert(f); rc = fsl_deck_fossilize(os, (unsigned char const *)f->name, -1); if(!rc) rc = fsl_deck_append( os, "F %b", &os->scratch); if(!rc && f->uuid){ assert(fsl_is_uuid(f->uuid)); rc = fsl_deck_append( os, " %s", f->uuid); if(rc) return rc; } hasOldName = f->priorName && (0!=fsl_strcmp(f->name,f->priorName)); switch(f->perms){ case FSL_MF_PERM_EXE: zPerm = " x"; break; case FSL_MF_PERM_LINK: zPerm = " l"; break; default: /* When hasOldName, we have to inject an otherwise optional 'w' to avoid an ambiguity. Or at least that's what the v1 F-card-generating code i looked at does. */ zPerm = hasOldName ? " w" : ""; break; } if(*zPerm) rc = fsl_deck_append( os, "%s", zPerm); if(!rc && hasOldName){ assert(*zPerm); rc = fsl_deck_fossilize(os, (unsigned char const *)f->priorName, -1); if(!rc) rc = fsl_deck_append( os, " %b", &os->scratch); } if(!rc) fsl_appendf_f_mf(os, "\n", 1); return os->rc; } static int fsl_deck_out_list_obj( fsl_deck_out_state * os, fsl_list const * li, fsl_list_visitor_f visitor ){ if(li->used){ os->rc = fsl_list_visit( li, 0, visitor, os ); } return os->rc; } static int fsl_deck_out_F( fsl_deck_out_state * os ){ return fsl_deck_out_list_obj(os, &os->cards->F.list, fsl_list_v_mf_output_card_F); } /** ** A comparison routine for qsort(3) which compares fsl_card_J ** instances in a lexical manner based on their names. The order is ** important for card ordering in generated manifests. */ static int qsort_cmp_J_cards( void const * lhs, void const * rhs ){ fsl_card_J const * l = *((fsl_card_J const **)lhs); fsl_card_J const * r = *((fsl_card_J const **)rhs); /* Compare NULL as larger so that NULLs move to the right. That said, we aren't expecting any NULLs. */ assert(l); assert(r); if(!l) return r ? 1 : 0; else if(!r) return -1; else{ /* The '+' sorts before any legal field name bits (letters). */ if(l->isAppend != r->isAppend) return r->isAppend - l->isAppend /* Footnote: that will break if, e.g. l->isAppend==2 and r->isAppend=1, or some such. Shame C89 doesn't have a true boolean. */; else return fsl_strcmp(l->field, r->field); } } /** ** fsl_list_visitor_f() impl for outputing J cards. obj must ** be a (fsl_card_J *). */ static int fsl_list_v_mf_output_card_J(void * obj, void * visitorState ){ fsl_deck_out_state * os = (fsl_deck_out_state *)visitorState; fsl_card_J const * c = (fsl_card_J const *)obj; fsl_deck_fossilize( os, (unsigned char const *)c->field, -1 ); if(!os->rc){ fsl_deck_append(os, "J %s%b", c->isAppend ? "+" : "", &os->scratch); if(!os->rc){ if(c->value && *c->value){ fsl_deck_fossilize( os, (unsigned char const *)c->value, -1 ); if(!os->rc){ fsl_deck_append(os, " %b\n", &os->scratch); } }else{ fsl_deck_append(os, "\n"); } } } return os->rc; } static int fsl_deck_out_J( fsl_deck_out_state * os ){ return fsl_deck_out_list_obj(os, &os->cards->J, fsl_list_v_mf_output_card_J); } /* Appends the K card to os from os->cards->K. */ static int fsl_deck_out_K( fsl_deck_out_state * os ){ if(os->cards->K){ if(!fsl_is_uuid(os->cards->K)){ os->rc = fsl_error_set(&os->error, FSL_RC_RANGE, "Invalid UUID in K card."); } else{ fsl_deck_append(os, "K %s\n", os->cards->K); } } return os->rc; } /* Appends the L card to os from os->cards->L. */ static int fsl_deck_out_L( fsl_deck_out_state * os ){ if(os->cards->L.used){ fsl_deck_out_letter_buf(os, "L", &os->cards->L, 1); } return os->rc; } /* Appends the N card to os from os->cards->N. */ static int fsl_deck_out_N( fsl_deck_out_state * os ){ if(os->cards->N.used){ fsl_deck_out_letter_buf(os, "N", &os->cards->N, 1); } return os->rc; } /** ** fsl_list_visitor_f() impl for outputing P cards. obj must ** be a (fsl_deck_out_state *) and obj->counter must be ** set to 0 before running the visit iteration. */ static int fsl_list_v_mf_output_card_P(void * obj, void * visitorState ){ fsl_deck_out_state * os = (fsl_deck_out_state *)visitorState; char const * uuid = (char const *)obj; fsl_size_t len; assert(uuid); if(!fsl_is_uuid(uuid)){ os->rc = fsl_error_set(&os->error, FSL_RC_RANGE, "Invalid UUID in P card."); } else if(!os->counter++) fsl_appendf_f_mf(os, "P ", 2); else fsl_appendf_f_mf(os, " ", 1); /* Reminder: fsl_appendf_f_mf() updates os->rc. */ if(!os->rc){ len = (fsl_size_t)fsl_strlen(uuid); assert(FSL_UUID_STRLEN==len) /* is enforced by fsl_mf_add_P() */; fsl_appendf_f_mf(os, uuid, len); } return os->rc; } static int fsl_deck_out_P( fsl_deck_out_state * os ){ if(os->cards->P.used){ os->counter = 0; os->rc = fsl_list_visit( &os->cards->P, 0, fsl_list_v_mf_output_card_P, os ); assert(os->counter); if(!os->rc) fsl_appendf_f_mf(os, "\n", 1); } #if 0 else if(1==os->cards->rid){ /* EVIL UGLY HACK for manifest #1, which has an empty P card! This kludge is not useful because at this point this would normally be used, we don't yet have the RID! It's only useful for parsing tests. */ fsl_deck_append(os, "P\n"); } #endif return os->rc; } /** ** A comparison routine for qsort(3) which compares fsl_card_Q ** instances in a lexical manner. The order is important for card ** ordering in generated manifests. */ static int qsort_cmp_Q_cards( void const * lhs, void const * rhs ){ fsl_card_Q const * l = *((fsl_card_Q const **)lhs); fsl_card_Q const * r = *((fsl_card_Q const **)rhs); /* Compare NULL as larger so that NULLs move to the right. That said, we aren't expecting any NULLs. */ assert(l); assert(r); if(!l) return r ? 1 : 0; else if(!r) return -1; else{ /* Lexical sorting must account for the +/- characters, and a '+' sorts before '-', which is why this next part may seem backwards at first. */ assert(l->type); assert(r->type); if(l->type<0 && r->type>0) return 1; else if(l->type>0 && r->type<0) return -1; else return fsl_strcmp(l->target, r->target); } } /** ** fsl_list_visitor_f() impl for outputing Q cards. obj must ** be a (fsl_deck_out_state *) and obj->counter must be ** set to 0 before running this visit iteration. */ static int fsl_list_v_mf_output_card_Q(void * obj, void * visitorState ){ fsl_deck_out_state * os = (fsl_deck_out_state *)visitorState; fsl_card_Q const * cp = (fsl_card_Q const *)obj; char const prefix = (cp->type<0) ? '-' : '+'; assert(cp->type); assert(cp->target); if(!cp->type){ os->rc = fsl_error_set(&os->error, FSL_RC_RANGE, "Invalid type value in Q-card."); } else if(!fsl_is_uuid(cp->target)){ os->rc = fsl_error_set(&os->error, FSL_RC_RANGE, "Invalid target UUID in Q-card."); } else if(cp->baseline){ if(!fsl_is_uuid(cp->baseline)){ os->rc = fsl_error_set(&os->error, FSL_RC_RANGE, "Invalid baseline UUID in Q-card."); }else{ fsl_deck_append(os, "Q %c%s %s\n", prefix, cp->target, cp->baseline); } }else{ fsl_deck_append(os, "Q %c%s\n", prefix, cp->target); } return os->rc; } static int fsl_deck_out_Q( fsl_deck_out_state * os ){ return fsl_deck_out_list_obj(os, &os->cards->Q, fsl_list_v_mf_output_card_Q); } /** ** Appends the R card from os->cards->R to os. */ static int fsl_deck_out_R( fsl_deck_out_state * os ){ if(os->cards->R){ if(FSL_MD5_STRLEN!=fsl_strlen(os->cards->R)){ os->rc = fsl_error_set(&os->error, FSL_RC_RANGE, "Malformed MD5 in R-card."); } else{ fsl_deck_append(os, "R %s\n", os->cards->R); } } return os->rc; } /** ** fsl_list_visitor_f() impl for outputing T cards. obj must ** be a (fsl_deck_out_state *). */ static int fsl_list_v_mf_output_card_T(void * obj, void * visitorState ){ fsl_deck_out_state * os = (fsl_deck_out_state *)visitorState; fsl_card_T * t = (fsl_card_T *)obj; char prefix = 0; /* Determine the prefix character... */ switch(t->type){ case FSL_TAGTYPE_CANCEL: prefix = '-'; break; case FSL_TAGTYPE_ADD: prefix = '+'; break; case FSL_TAGTYPE_PROPAGATING: prefix = '*'; break; default: return os->rc = fsl_error_set(&os->error, FSL_RC_TYPE, "Invalid tag type #%d in T-card.", t->type); } /* Fossilize and output the prefix, name, and uuid, or a '*' if no uuid is set (which is only legal when tagging the current artifact, as '*' is a placeholder for the current artifact's UUID, which is not yet known). */ os->scratch.used = 0; fsl_deck_fossilize_b(os, &t->name); if(os->rc) return os->rc; if(t->uuid && !fsl_is_uuid(t->uuid)){ return os->rc = fsl_error_set(&os->error, FSL_RC_TYPE, "Malformed UUID in T-card: %s", t->uuid); } os->rc = fsl_deck_append(os, "T %c%s %s", prefix, (char const*)os->scratch.mem, t->uuid ? t->uuid : "*"); if(os->rc) return os->rc; if(/*(t->type != FSL_TAGTYPE_CANCEL) &&*/t->value.used){ /* CANCEL tags historically don't store a value but the spec doesn't disallow it and they are harmless for (aren't used by) fossil(1). */ fsl_deck_fossilize_b(os, &t->value); if(!os->rc) fsl_appendf_f_mf(os, " ", 1); if(!os->rc) fsl_appendf_f_mf(os, (char const*)os->scratch.mem, (fsl_int_t)os->scratch.used); } if(!os->rc){ fsl_appendf_f_mf(os, "\n", 1); } return os->rc; } /* Appends the U card to os from os->cards->U. */ static int fsl_deck_out_U( fsl_deck_out_state * os ){ if(os->cards->U.used){ fsl_deck_out_letter_buf(os, "U", &os->cards->U, 1); } return os->rc; } /** ** Returns one of '-', '+', or '*' for a valid input parameter, 0 for ** any other value. */ static char fsl_tag_prefix_char( fsl_tag_type t ){ switch(t){ case FSL_TAGTYPE_CANCEL: return '-'; case FSL_TAGTYPE_ADD: return '+'; case FSL_TAGTYPE_PROPAGATING: return '*'; default: return 0; } } /** ** A comparison routine for qsort(3) which compares fsl_card_T ** instances in a lexical manner based on (type, name, uuid, value). ** The order of those is important for card ordering in generated ** manifests. */ static int qsort_cmp_T_cards( void const * lhs, void const * rhs ){ fsl_card_T const * l = *((fsl_card_T const **)lhs); fsl_card_T const * r = *((fsl_card_T const **)rhs); /* Compare NULL as larger so that NULLs move to the right. That said, we aren't expecting any NULLs. */ assert(l); assert(r); if(!l) return r ? 1 : 0; else if(!r) return -1; else if(l->type != r->type){ char const lc = fsl_tag_prefix_char(l->type); char const rc = fsl_tag_prefix_char(r->type); return (lcname, &r->name); if(rc) return rc; else { rc = fsl_uuidcmp(l->uuid, r->uuid); return rc ? rc : fsl_buffer_compare(&l->value, &r->value); } } } static int fsl_deck_out_T( fsl_deck_out_state * os ){ return fsl_deck_out_list_obj(os, &os->cards->T, fsl_list_v_mf_output_card_T); } static int fsl_deck_out_W( fsl_deck_out_state * os ){ if(os->cards->W.used){ fsl_deck_append(os, "W %"FSL_SIZE_T_PFMT"\n%b\n", os->cards->W.used, &os->cards->W ); } return os->rc; } /* Appends the Z card to os from os' accummulated md5 hash. */ static int fsl_deck_out_Z( fsl_deck_out_state * os ){ unsigned char digest[16]; char md5[FSL_MD5_STRLEN+1]; fsl_md5_final(&os->md5, digest); fsl_md5_digest_to_base16(digest, md5); assert(!md5[32]); os->md5.isInit = 0 /* Keep further output from updating the MD5 */; return fsl_deck_append(os, "Z %.*s\n", FSL_MD5_STRLEN, md5); } static char fsl_deck_check_required_cards(fsl_cx *f, fsl_deck const * d){ if(fsl_deck_has_required_cards(d)) return 1; else{ fsl_cx_err_set(f, FSL_RC_CA_SYNTAX, "Deck of type '%s' is missing one " "or more required cards.", fsl_catype_cstr(d->type)); return 0; } } static int qsort_cmp_strings( void const * lhs, void const * rhs ){ char const * l = *((char const **)lhs); char const * r = *((char const **)rhs); return fsl_strcmp(l,r); } static int fsl_list_v_mf_output_card_M(void * obj, void * visitorState ){ fsl_deck_out_state * os = (fsl_deck_out_state *)visitorState; char const * m = (char const *)obj; return fsl_deck_append(os, "M %s\n", m); } static int fsl_deck_output_cluster( fsl_deck_out_state * os ){ return fsl_deck_out_list_obj(os, &os->cards->M, fsl_list_v_mf_output_card_M); } /* Helper for fsl_deck_output_CATYPE() */ #define DOUT(LETTER) rc = fsl_deck_out_##LETTER(os); \ if(rc || os->rc) return os->rc ? os->rc : rc static int fsl_deck_output_control( fsl_deck_out_state * os ){ int rc; /* Reminder: cards must be output in strict lexical order. */ DOUT(D); DOUT(P); DOUT(T); DOUT(U); return os->rc; } static int fsl_deck_output_event( fsl_deck_out_state * os ){ int rc = 0; /* Reminder: cards must be output in strict lexical order. */ DOUT(C); DOUT(D); DOUT(E); DOUT(N); DOUT(P); DOUT(T); DOUT(U); DOUT(W); return os->rc; } static int fsl_deck_output_mf( fsl_deck_out_state * os ){ int rc = 0; /* Reminder: cards must be output in strict lexical order. */ DOUT(B); DOUT(C); DOUT(D); DOUT(F); DOUT(K); DOUT(L); DOUT(N); DOUT(P); DOUT(Q); DOUT(R); DOUT(T); DOUT(U); DOUT(W); return os->rc; } static int fsl_deck_output_ticket( fsl_deck_out_state * os ){ int rc; /* Reminder: cards must be output in strict lexical order. */ DOUT(D); DOUT(J); DOUT(K); DOUT(U); return os->rc; } static int fsl_deck_output_wiki( fsl_deck_out_state * os ){ int rc; /* Reminder: cards must be output in strict lexical order. */ DOUT(D); DOUT(L); DOUT(N); DOUT(P); DOUT(U); DOUT(W); return os->rc; } /** ** Only for testing/debugging purposes, as it allows constructs which ** are not semantically legal and are CERTAINLY not legal to stuff in ** the database. */ static int fsl_deck_output_any( fsl_deck_out_state * os ){ int rc = 0; /* Reminder: cards must be output in strict lexical order. */ DOUT(B); DOUT(C); DOUT(D); DOUT(E); DOUT(F); DOUT(J); DOUT(K); DOUT(L); DOUT(N); DOUT(P); DOUT(Q); DOUT(R); DOUT(T); DOUT(U); DOUT(W); return os->rc; } #undef DOUT int fsl_deck_unshuffle( fsl_deck * d, char calculateRCard ){ fsl_list * li; int rc = 0; if(!d) return FSL_RC_MISUSE; #define SORT(CARD,CMP) li = &d->CARD; fsl_list_sort(li, CMP ) SORT(J,qsort_cmp_J_cards); SORT(M,qsort_cmp_strings); SORT(Q,qsort_cmp_Q_cards); SORT(T,qsort_cmp_T_cards); #undef SORT if(fsl_card_is_legal(d->type, 'R')){ assert(FSL_CATYPE_MANIFEST==d->type); if(calculateRCard){ rc = fsl_deck_R_calc(d) /* F-card list is sorted there */; }else{ fsl_deck_F_sort(d); rc = fsl_deck_R_set(d, (d->F.list.used || d->B.uuid || d->P.used) ? NULL : FSL_MD5_INITIAL_HASH) /* Special case: for manifests with no (B,F,P)-cards we inject the initial-state R-card, analog to the initial checkin (RID 1). We need one of (B,F,P,R) to unambiguously identify a MANIFEST from a CONTROL, but RID 1 has an empty P-card (which this API can read but refuses to output because recognizing this special case is a chicken/egg scenario), no F-cards, and no B-card, so it _needs_ an R-card in order to be unambiguously a Manifest. */ ; assert(!rc); } } return rc; } int fsl_deck_output( fsl_cx * f, fsl_deck const * cards, fsl_output_f out, void * outputState ){ static const char allowTypeAny = 0 /* Only enable for debugging/testing. Allows outputing decks of ** type FSL_CATYPE_ANY, which bypasses some validation checks and ** may trigger other validation assertions. And may allow you to ** inject garbage into the repo. So be careful. */; fsl_deck_out_state OS = fsl_deck_out_state_empty; fsl_deck_out_state * os = &OS; int rc = 0; fsl_db * db = f ? f->dbMain : NULL; /* Reminder to self: we need SOME db handle for Julian date conversions (that's the only thing we need it for, too). We don't care which one we use. */ ; if(!cards || !out || !f || !db) return FSL_RC_MISUSE; else if(!fsl_deck_check_required_cards(f,cards)){ return FSL_RC_CA_SYNTAX; } else if(FSL_CATYPE_ANY==cards->type){ if(!allowTypeAny){ return fsl_cx_err_set(f, FSL_RC_TYPE, "Control artifact type ANY cannot be " "output unless it is enabled in this " "code (it's dangerous)."); } /* fall through ... */ } os->db = db; os->cards = cards; os->out = out; os->outState = outputState; switch(cards->type){ case FSL_CATYPE_CLUSTER: rc = fsl_deck_output_cluster(os); break; case FSL_CATYPE_CONTROL: rc = fsl_deck_output_control(os); break; case FSL_CATYPE_EVENT: rc = fsl_deck_output_event(os); break; case FSL_CATYPE_MANIFEST: rc = fsl_deck_output_mf(os); break; case FSL_CATYPE_TICKET: rc = fsl_deck_output_ticket(os); break; case FSL_CATYPE_WIKI: rc = fsl_deck_output_wiki(os); break; case FSL_CATYPE_ANY: assert(allowTypeAny); rc = fsl_deck_output_any(os); break; default: rc = fsl_cx_err_set(f, FSL_RC_TYPE, "Invalid/unhandled deck type (#%d).", cards->type); goto end; } if(!rc){ rc = fsl_deck_out_Z( os ); } end: fsl_buffer_clear(&os->scratch); return os->rc ? os->rc : rc; } int fsl_deck_crosslink_end(fsl_cx * f){ int rc = 0; fsl_db * db = fsl_cx_db_repo(f); assert(f); assert(db); assert(f->cache.isCrosslinking); if(!f->cache.isCrosslinking) return FSL_RC_RANGE; f->cache.isCrosslinking = 0; /* TODO: port in manifest.c:manifest_crosslink_end() */ return rc; } int fsl_deck_crosslink_begin(fsl_cx * f){ int rc; fsl_db * db = fsl_cx_db_repo(f); assert(f); assert(db); assert(0==f->cache.isCrosslinking); if(f->cache.isCrosslinking) return FSL_RC_RANGE; rc = fsl_db_transaction_begin(db); if(rc) return rc; rc = fsl_db_exec_multi(db, "CREATE TEMP TABLE pending_tkt(uuid TEXT UNIQUE);" "CREATE TEMP TABLE time_fudge(" " mid INTEGER PRIMARY KEY," /* The rid of a manifest */ " m1 REAL," /* The timestamp on mid */ " cid INTEGER," /* A child or mid */ " m2 REAL" /* Timestamp on the child */ ");" ); if(!rc) f->cache.isCrosslinking = 1; return rc; } /** @internal ** ** Add a single entry to the mlink table. Also add the filename to ** the filename table if it is not there already. ** ** Parameters: ** ** mid: The record ID of the manifest ** ** zFromUuid: UUID for the mlink.pid. "" to add file ** ** zToUuid:UUID for the mlink.fid. "" to delete ** ** zFilename: Filename ** ** zPrior: Previous filename. NULL if unchanged ** ** isPublic:True if mid is not a private manifest ** ** mperm: permissions */ static int fsl_repo_mlink_add_one( fsl_cx * f, fsl_id_t mid, fsl_uuid_cstr zFromUuid, fsl_uuid_cstr zToUuid, char const * zFilename, char const * zPrior, char isPublic, fsl_mf_perm_t mperm){ fsl_id_t fnid, pfnid, pid, fid; fsl_db * db = fsl_cx_db_repo(f); fsl_stmt * s1 = NULL; int rc; assert(f); assert(db); assert(db->beginCount>0); rc = fsl_repo_filename_to_fnid(f, zFilename, &fnid); if(rc) return rc; if( zPrior && *zPrior ){ rc = fsl_repo_filename_to_fnid(f, zPrior, &pfnid); if(rc) return rc; }else{ pfnid = 0; } if( zFromUuid && *zFromUuid ){ pid = fsl_repo_uuid_to_rid2(f, zFromUuid, FSL_PHANTOM_PUBLIC); if(pid<0){ assert(f->error.code); return f->error.code; } assert(pid>0); }else{ pid = 0; } if( zToUuid && *zToUuid ){ fid = fsl_repo_uuid_to_rid2(f, zToUuid, FSL_PHANTOM_PUBLIC); if( isPublic ){ rc = fsl_content_make_public(f, fid); if(rc) return rc; } }else{ fid = 0; } rc = fsl_db_prepare_cached(db, &s1, "INSERT INTO mlink(" "mid,pid,fid," "fnid,pfnid,mperm" ")VALUES(" ":m,:p,:f,:n,:pfn,:mp" ")"); if(!rc){ fsl_stmt_bind_id_name(s1, ":m", mid); fsl_stmt_bind_id_name(s1, ":p", pid); fsl_stmt_bind_id_name(s1, ":f", fid); fsl_stmt_bind_id_name(s1, ":n", fnid); fsl_stmt_bind_id_name(s1, ":pfn", pfnid); fsl_stmt_bind_id_name(s1, ":mp", mperm); rc = fsl_stmt_step(s1); fsl_stmt_cached_yield(s1); if(FSL_RC_STEP_DONE==rc){ rc = 0; if(pid && fid){ rc = fsl_content_deltify(f, pid, fid, 0); } } } return rc; } /** ** Do a binary search to find a file in d->F.list. ** ** As an optimization, guess that the file we seek is at index p->iFile. ** That will usually be the case. If it is not found there, then do the ** actual binary search. ** ** Update d->F.cursor to be the index of the file that is found. ** ** Reminder to self: if this requires a non-const deck (and it does ** right now) then the whole downstream chain will require a ** non-const instance or they'll have to make local copies to make ** the manipulation of d->F.cursor legal (but that would break ** following of baselines without yet more trickery). */ static fsl_card_F const * fsl_deck_F_seek_base(fsl_deck * d, char const * zName){ fsl_size_t lwr, upr; int c; fsl_size_t i; #define FCARD(NDX) ((fsl_card_F const *)d->F.list.list[NDX]) lwr = 0; upr = d->F.list.used ? d->F.list.used-1 : 0; if( d->F.cursor>=lwr && d->F.cursorF.cursor+1)->name, zName); if( c==0 ){ return FCARD(++d->F.cursor); }else if( c>0 ){ upr = d->F.cursor; }else{ lwr = d->F.cursor+1; } } while( lwr<=upr ){ i = (lwr+upr)/2; c = fsl_strcmp(FCARD(i)->name, zName); if( c<0 ){ lwr = i+1; }else if( c>0 ){ upr = i-1; }else{ d->F.cursor = i; return FCARD(i); } } return NULL; #undef FCARD } /** ** Locate a file named zName in d->F.list. Return a pointer to the ** appropriate fsl_mf_card object. Return NULL if not found. ** ** This routine works even if p is a delta-manifest. The pointer ** returned might be to the baseline and d->B.baseline is loaded on ** demand if needed. ** ** Returns NULL if it finds a match but that match has no UUID, ** indicating that it was removed from the tree by this ** manifest. That behaviour is arguable (but historic and some ** code relies on it). ** ** We assume that filenames are in sorted order and use a binary ** search. */ static fsl_card_F const * fsl_deck_F_seek(fsl_deck * d, const char *zName){ fsl_card_F const *pFile; again: pFile = fsl_deck_F_seek_base(d, zName); #if 1 /* i don't much like this, but don't (at the moment) want to go change/test the baseline traverse code which might rely on this. i'd prefer to let the caller deal with this case, as eliding this leaves info out of some loops where we want them. */ if( pFile && !pFile->uuid ) return NULL /* File was removed from repo in this manifest. */ ; else #endif if( !pFile && d && d->B.uuid ){ /* Recursively check baseline manifests... */ int rc; /* assert(!"force caller to load baseline so this op can be const."); */ rc = fsl_deck_baseline_fetch(d); if(rc){ assert(d->f->error.code); }else if( (d = d->B.baseline) ) goto again; } return pFile; } /** ** Um... i ported this in here for a reason and now i cannot find ** that it's actually being used somewhere. */ /** ** fsl_list_visitor_f() state for fsl_deck_F_search(). */ struct FCardSearch { /** Filename to match against */ char const * str; /** On success, this is set to the found object. */ fsl_card_F const * fc; }; typedef struct FCardSearch FCardSearch; /** ** A fsl_list_visitor_f() impl which performs a case-insensitive ** search for a file. state must be a (FCardSearch*). state->str ** is the string to search for. On success, FSL_RC_BREAK is returned ** and state->fc will point to the F-card which was found. */ static int fsl_list_v_fsl_mf_F_search_nocase(void * obj, void * state ){ fsl_card_F const * fc = (fsl_card_F const *)obj; FCardSearch * st = (FCardSearch*)state; int rc = 0; if(0==fsl_stricmp(fc->name, st->str)){ #if 0 /* v1 does _not_ skip entries where !fc->uuid for the case-insenstive search. i think this is a bug/oversight, but _suspect_ that the cases it's used don't require this distinction. To make this equivalent to the case-sensitive search we need the following: */ if(!fc->uuid) break; #endif st->fc = fc; rc = FSL_RC_BREAK; } return rc; } /** ** Look for a file in a manifest, taking the case-insensitive option ** into account. If case-insensitive is on then files in any case ** will match. ** ** Don't make this public until: ** ** A) we patch the case-insensitive search to behave like the non-CS ** search (case-insensitive does not ignore 'removed' (uuid==NULL) ** entries like CS does). ** ** Or: ** ** B) or change the case-sensitive semantics to return files which ** have no UUID (==they were removed via this manifest). ** ** (B) is my preference but the change needs testing in places which ** are relatively intricate. */ /*static*/ fsl_card_F const * fsl_deck_F_search(fsl_deck *d, const char *zName){ assert(d->f); if( !d->f->cache.caseInsensitive ) return fsl_deck_F_seek(d, zName); else{ FCardSearch state; state.str = zName; state.fc = NULL; while(d && !state.fc){ fsl_list_visit(&d->F.list, 0, fsl_list_v_fsl_mf_F_search_nocase, &state); if(!state.fc && d->B.uuid){ /* assert(!"force caller to load baseline so this op can be const."); */ fsl_deck_baseline_fetch(d); d = d->B.baseline; }else{ d = NULL; } } return state.fc; } } /** ** Add mlink table entries associated with manifest cid, pChild. The ** parent manifest is pid, pParent. One of either pChild or pParent ** will be NULL and it will be computed based on cid/pid. ** ** A single mlink entry is added for every file that changed content, ** name, and/or permissions going from pid to cid. ** ** Deleted files have mlink.fid=0. ** ** Added files have mlink.pid=0. ** ** Edited files have both mlink.pid!=0 and mlink.fid!=0 */ static int fsl_repo_mlink_add( fsl_cx * f, fsl_id_t pid, fsl_deck /*const*/ * pParent, fsl_id_t cid, fsl_deck /*const*/ * pChild){ fsl_buffer otherContent = fsl_buffer_empty; fsl_id_t otherRid; fsl_size_t i = 0; int rc = 0; fsl_card_F * pChildFile = NULL, * pParentFile = NULL; fsl_deck ** ppOther; fsl_stmt * st = NULL; fsl_db * db = fsl_cx_db_repo(f); char isPublic; assert(db); assert(db->beginCount>0); /* If mlink table entires are already set for cid, then abort early ** doing no work. */ rc = fsl_db_prepare_cached(db, &st, "SELECT 1 FROM mlink WHERE mid=?"); if(rc) return rc; fsl_stmt_bind_id(st, 1, cid); rc = fsl_stmt_step(st); fsl_stmt_cached_yield(st); if( rc==FSL_RC_STEP_ROW ) return 0; /* Compute the value of the missing pParent or pChild parameter. ** Fetch the baseline checkins for both. */ assert( pParent==0 || pChild==0 ); if( pParent ){ ppOther = &pChild; otherRid = cid; }else{ ppOther = &pParent; otherRid = pid; } rc = fsl_content_get(f, otherRid, &otherContent); if(rc) goto end; if( !otherContent.used ){ /* Is zero correct here? Can this happen? Loaded non-manifest?*/ return 0; } *ppOther = fsl_deck_malloc(); if(!*ppOther){ rc = FSL_RC_OOM; goto end; } rc = fsl_deck_parse(f, otherRid, *ppOther, &otherContent); if(rc) goto end; if( (rc=fsl_deck_baseline_fetch(pParent)) || (rc=fsl_deck_baseline_fetch(pChild))){ goto end; } rc = 0; isPublic = !fsl_content_is_private(f, cid); /* Try to make the parent manifest a delta from the child, if that ** is an appropriate thing to do. For a new baseline, make the ** previous baseline a delta from the current baseline. */ if( (pParent->B.uuid==0)==(pChild->B.uuid==0) ){ rc = fsl_content_deltify(f, pid, cid, 0); }else if( pChild->B.uuid==NULL && pParent->B.uuid!=NULL ){ rc = fsl_content_deltify(f, pParent->B.baseline->rid, cid, 0); } if(rc) goto end; #if 0 /* TODO (eventually): port these bits in. They require the crosslink_begin/end() infrastructure. Not needed until we support tickets. */ /* Remember all children less than a few seconds younger than their parent, ** as we might want to fudge the times for those children. */ if( pChild->rDaterDate+AGE_FUDGE_WINDOW && manifest_crosslink_busy ){ db_multi_exec( "INSERT OR REPLACE INTO time_fudge VALUES(%d, %"FSL_JULIAN_T_PFMT", %d, %"FSL_JULIAN_T_PFMT");", pParent->rid, pParent->rDate, pChild->rid, pChild->rDate ); } #endif /* First look at all files in pChild, ignoring its baseline. This ** is where most of the changes will be found. */ #define FCARD(DECK,NDX) \ ((((NDX)<(DECK)->F.list.used)) \ ? ((fsl_card_F *)(DECK)->F.list.list[NDX]) \ : NULL) /* First look at all files in pChild, ignoring its baseline. This ** is where most of the changes will be found. */ for(i=0, pChildFile=FCARD(pChild,0); iF.list.used; ++i, pChildFile=FCARD(pChild,i)){ fsl_mf_perm_t const mperm = pChildFile->perms; if( pChildFile->priorName ){ pParentFile = (fsl_card_F*) fsl_deck_F_seek(pParent, pChildFile->priorName); if( pParentFile ){ /* File with name change */ rc = fsl_repo_mlink_add_one(f, cid, pParentFile->uuid, pChildFile->uuid, pChildFile->name, pChildFile->priorName, isPublic, mperm); }else{ /* File name changed, but the old name is not found in the parent! ** Treat this like a new file. */ rc = fsl_repo_mlink_add_one(f, cid, 0, pChildFile->uuid, pChildFile->name, 0, isPublic, mperm); } }else{ pParentFile = (fsl_card_F*) fsl_deck_F_seek(pParent, pChildFile->name); if( pParentFile==0 ){ if( pChildFile->uuid ){ /* A new file */ rc = fsl_repo_mlink_add_one(f, cid, 0, pChildFile->uuid, pChildFile->name, 0, isPublic, mperm); } } else if( fsl_strcmp(pChildFile->uuid, pParentFile->uuid)!=0 || (pParentFile->perms!=mperm) ){ /* Changes in file content or permissions */ rc = fsl_repo_mlink_add_one(f, cid, pParentFile->uuid, pChildFile->uuid, pChildFile->name, 0, isPublic, mperm); } } } /* end pChild card list loop */ if(rc) goto end; else if( pParent->B.uuid && pChild->B.uuid ){ /* Both parent and child are delta manifests. Look for files that ** are deleted or modified in the parent but which reappear or revert ** to baseline in the child and show such files as being added or changed ** in the child. */ for(i=0, pParentFile=FCARD(pParent,0); iF.list.used; ++i, pParentFile = FCARD(pParent,i)){ if( pParentFile->uuid ){ pChildFile = (fsl_card_F*) fsl_deck_F_seek_base(pChild, pParentFile->name); if( !pChildFile ){ /* The child file reverts to baseline. Show this as a change */ pChildFile = (fsl_card_F*) fsl_deck_F_seek(pChild, pParentFile->name); if( pChildFile ){ rc = fsl_repo_mlink_add_one(f, cid, pParentFile->uuid, pChildFile->uuid, pChildFile->name, 0, isPublic, pChildFile->perms); } } }else{ pChildFile = (fsl_card_F*) fsl_deck_F_seek(pChild, pParentFile->name); if( pChildFile ){ /* File resurrected in the child after having been deleted in ** the parent. Show this as an added file. */ rc = fsl_repo_mlink_add_one(f, cid, 0, pChildFile->uuid, pChildFile->name, 0, isPublic, pChildFile->perms); } } } }else if( !pChild->B.uuid ){ fsl_card_F const * cfc = NULL; /* pChild is a baseline. Look for files that are present in pParent ** but are missing from pChild and mark them as having been deleted. */ fsl_deck_F_rewind(pParent); while( (0==(rc=fsl_deck_F_next(pParent,&cfc))) && cfc){ pParentFile = (fsl_card_F*) cfc; pChildFile = (fsl_card_F*) fsl_deck_F_seek(pChild, pParentFile->name); if( !pChildFile && pParentFile->uuid ){ rc = fsl_repo_mlink_add_one(f, cid, pParentFile->uuid, 0, pParentFile->name, 0, isPublic, FSL_MF_PERM_REGULAR); } } } end: if(*ppOther){ /* TODO? cache *ppOther here, like v1 does. */ fsl_deck_finalize(*ppOther); } fsl_buffer_clear(&otherContent); return rc; #undef FCARD } int fsl_deck_crosslink( fsl_cx * f, fsl_deck /* const */ * d ){ int rc; fsl_db * db = fsl_cx_db_repo(f); fsl_id_t parentid = 0; fsl_double_t mtime; fsl_int_t const rid = d ? d->rid : -1; /* TODO: split this beast up into multiple (static) routines. We now have fsl_xlink_listener(), so we can at least farm out the parts which update the event table. */ assert(f); assert(d); assert(d->f == f); if(!f || !d) return FSL_RC_MISUSE; else if(rid<=0){ return fsl_cx_err_set(f, FSL_RC_RANGE, "Invalid RID for crosslink: %"FSL_ID_T_PFMT, rid); } else if(!db) return FSL_RC_NOT_A_REPO; else if(!fsl_deck_has_required_cards(d)){ return fsl_cx_err_set(f, FSL_RC_MISUSE, "Deck is missing one or more " "required cards."); } if(f->cache.xlinkClustersOnly && (FSL_CATYPE_CLUSTER==d->type)){ return 0; } mtime = (d->D>0) ? d->D : fsl_db_julian_now(db); if(FSL_CATYPE_MANIFEST==d->type){ /* TODO: try to load baseline, and fail if we can't. Doh, deck is const. So require that the caller to load it. Or load it automatically somewhere along the line, maybe at parse-time. But since it has to be non-const for other reasons, we'll go ahead and load the baseline here... */ if(d->B.uuid && !d->B.baseline){ #if 1 rc = fsl_deck_baseline_fetch(d); if(rc) goto end; assert(d->B.baseline); #else return fsl_cx_err_set(f, FSL_RC_MISUSE, "Baseline of delta manifest [%.*s] must be " "loaded before crosslinking. It is (or should be!) " "const in this context, so we cannot do it for " "you here.", 12, d->uuid); #endif } } rc = fsl_db_transaction_begin(db); if(rc) goto end; if(FSL_CATYPE_MANIFEST == d->type){ fsl_size_t i; fsl_stmt q = fsl_stmt_empty; /* TODO: convert these queries to cached statements, for the sake of rebuild and friends. And bind() doubles instead of %.17g'ing them. */ if(!fsl_db_exists(db, "SELECT 1 FROM mlink " "WHERE mid=%"FSL_ID_T_PFMT, rid)){ /* legacy (see comments below): char *zCom; */ for(i=0; iP.used; i++){ char const * parentUuid = (char const *)d->P.list[i]; fsl_id_t pid = fsl_repo_uuid_to_rid2(f, parentUuid, FSL_PHANTOM_PUBLIC); if(pid<0){ assert(f->error.code); rc = f->error.code; goto end; } rc = fsl_db_exec(db, "INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime)" "VALUES(%"FSL_ID_T_PFMT", %"FSL_ID_T_PFMT ", %d, %"FSL_JULIAN_T_PFMT")", pid, rid, i==0, d->D); if(rc) goto end; if( i==0 ){ rc = fsl_repo_mlink_add(f, pid, 0, rid, d); parentid = pid; } } rc = fsl_db_prepare(db, &q, "SELECT cid FROM plink " "WHERE pid=%"FSL_ID_T_PFMT" AND isprim", rid); while( !rc && (FSL_RC_STEP_ROW==(rc=fsl_stmt_step(&q))) ){ fsl_id_t cid = fsl_stmt_g_id(&q, 0); assert(cid>0); rc = fsl_repo_mlink_add(f, rid, d, cid, NULL); } if(FSL_RC_STEP_DONE==rc) rc = 0; fsl_stmt_finalize(&q); if(rc) goto end; if( !d->P.used ){ /* For root files (files without parents) add mlink entries ** showing all content as new. */ char isPublic = !fsl_content_is_private(f, rid); for(i=0; !rc && (iF.list.used); ++i){ fsl_card_F const * fc = (fsl_card_F const *)d->F.list.list[i]; rc = fsl_repo_mlink_add_one(f, rid, 0, fc->uuid, fc->name, 0, isPublic, fc->perms); } } if(rc) goto end; rc = fsl_db_exec(db, "REPLACE INTO event(type,mtime,objid,user,comment," "bgcolor,euser,ecomment,omtime)" "VALUES('ci'," " coalesce(" " (SELECT julianday(value) FROM tagxref " "WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT")," " %"FSL_JULIAN_T_PFMT"" " )," " %"FSL_ID_T_PFMT",%B,%B," " (SELECT value FROM tagxref " "WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT " AND tagtype>0)," " (SELECT value FROM tagxref " "WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT ")," " (SELECT value FROM tagxref " "WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT "),%"FSL_JULIAN_T_PFMT");", /* The (fsl_id_t) casts here are to work around an inexplicable memory corruption i am seeing in the va_list handling, and kdbg doesn't work on Ubuntu 13.04 :-< */ (int)FSL_TAGID_DATE, (fsl_id_t)rid, d->D, (fsl_id_t)rid, &d->U, &d->C, (int)FSL_TAGID_BGCOLOR, (fsl_id_t)rid, (int)FSL_TAGID_USER, (fsl_id_t)rid, (int)FSL_TAGID_COMMENT, (fsl_id_t)rid, d->D ); if(rc) goto end; #if 0 /* Legacy: need an option or crosslink callback for this. Reserve name 'fsl-timeline-wiki-links' but we have the problem that the last_insert_rowid() is likely to not be valid after this point. So how do we communicate that to the callback? */ zCom = db_text(0, "SELECT coalesce(ecomment, comment) FROM event" " WHERE rowid=last_insert_rowid()"); wiki_extract_links(zCom, rid, 0, d->rDate, 1, WIKI_INLINE); free(zCom); #endif /* If this is a delta-manifest, record the fact that this repository ** contains delta manifests, to free the "commit" logic to generate ** new delta manifests. */ if( d->B.baseline && (f->cache.seenManifest <= 0) ){ f->cache.seenManifest = 1; rc = fsl_repo_set_int32(f, "seen-delta-manifest", 1); } }/*!exists mlink*/ }/*MANIFEST*/ else if( FSL_CATYPE_CLUSTER == d->type ){ /* Clean up the unclustered table... */ fsl_size_t i; fsl_stmt * st = NULL; rc = fsl_db_prepare_cached(db, &st, "DELETE FROM unclustered WHERE rid=?"); if(rc) goto end; assert(st); for( i = 0; i < d->M.used; ++i ){ fsl_id_t mid; char const * uuid = (char const *)d->M.list[i]; mid = fsl_repo_uuid_to_rid(f, uuid); if(mid>0){ fsl_stmt_bind_id(st, 1, mid); fsl_stmt_step(st); fsl_stmt_reset(st); } } fsl_stmt_cached_yield(st); }/*CLUSTER*/ if( d->type==FSL_CATYPE_CONTROL || d->type==FSL_CATYPE_MANIFEST || d->type==FSL_CATYPE_EVENT ){ /* Apply tags... */ fsl_size_t i; fsl_list const * li = &d->T; fsl_double_t tagTime = d->D; if(li->used){ if(tagTime<=0){ tagTime = fsl_db_julian_now(db); if(tagTime<=0){ rc = FSL_RC_DB; goto end; } } } for( i = 0; !rc && (i < li->used); ++i){ fsl_id_t tid; fsl_card_T const * tag = (fsl_card_T const *)li->list[i]; assert(tag); if(!tag->uuid){ tid = rid; }else{ tid = fsl_repo_uuid_to_rid( f, tag->uuid); } if(tid<=0){ rc = fsl_cx_err_set(f, FSL_RC_RANGE, "Could not get rid for [%s].", tag->uuid); goto end; } rc = fsl_repo_tag_insert(f, tag->type, fsl_buffer_cstr(&tag->name), fsl_buffer_cstr(&tag->value), rid, tagTime, tid, NULL); } if( !rc && (parentid>0) ){ rc = fsl_repo_tag_propagate_all(f, parentid); } }/*CONTROL | MANIFEST | EVENT*/ else if( d->type==FSL_CATYPE_WIKI ){ fsl_buffer buf = fsl_buffer_empty; fsl_id_t tagid; fsl_id_t prior; char zLength[40] = {0}; char const * zWiki; char const * zTag; fsl_size_t nWiki = 0; rc = fsl_buffer_appendf(&buf, "wiki-%b", &d->L); if(rc) goto wiki_end; zTag = fsl_buffer_cstr(&buf); tagid = fsl_repo_tag_id( f, zTag, 1 ); if(tagid<=0){ rc = f->error.code ? f->error.code : fsl_cx_err_set(f, FSL_RC_RANGE, "Got unexpected RID (%"FSL_ID_T_PFMT") " "for tag [%s].", tagid, zTag); goto wiki_end; } zWiki = d->W.used ? fsl_buffer_cstr(&d->W) : ""; while( *zWiki && fsl_isspace(*zWiki) ){ ++zWiki; /* Historical behaviour: strip leading spaces. */ } nWiki = fsl_strlen(zWiki) /* Reminder: use strlen instead of d->W.used in case just in case that one contains embedded NULs in the content. */; fsl_snprintf( zLength, sizeof(zLength), "%"FSL_SIZE_T_PFMT, nWiki); rc = fsl_repo_tag_insert(f, FSL_TAGTYPE_ADD, zTag, zLength, rid, d->D, rid, NULL ); if(rc) goto wiki_end; buf.used = 0 /* re-use memory for comment text */; zTag = zWiki = NULL; prior = fsl_db_g_id(db, 0, "SELECT rid FROM tagxref" " WHERE tagid=%"FSL_ID_T_PFMT" AND mtime<%"FSL_JULIAN_T_PFMT " ORDER BY mtime DESC", tagid, d->D ); if(prior>0){ rc = fsl_content_deltify(f, prior, rid, 0); if(rc) goto wiki_end; } /* WIKI constructs currently (201308) do not allow a C card. When/if they do, use it here. */ if( nWiki > 0 ){ rc = fsl_buffer_appendf(&buf, "Changes to wiki page [%h]", fsl_buffer_cstr(&d->L)); }else{ rc = fsl_buffer_appendf(&buf, "Deleted wiki page [%h]", fsl_buffer_cstr(&d->L)); } if(!rc){ fsl_db_exec(db, "REPLACE INTO event(type,mtime,objid,user,comment," " bgcolor,euser,ecomment) " "VALUES('w',%"FSL_JULIAN_T_PFMT",%"FSL_ID_T_PFMT",%B,%B," " (SELECT value FROM tagxref " "WHERE tagid=%"FSL_ID_T_PFMT" AND rid=%"FSL_ID_T_PFMT " AND tagtype>1)," " (SELECT value FROM tagxref " "WHERE tagid=%"FSL_ID_T_PFMT" AND rid=%"FSL_ID_T_PFMT")," " (SELECT value FROM tagxref WHERE " "tagid=%"FSL_ID_T_PFMT " AND rid=%"FSL_ID_T_PFMT"));", d->D, (fsl_id_t)rid, &d->U, &buf, FSL_TAGID_BGCOLOR, (fsl_id_t)rid, FSL_TAGID_BGCOLOR, (fsl_id_t)rid, FSL_TAGID_USER, (fsl_id_t)rid, FSL_TAGID_COMMENT, (fsl_id_t)rid ); } wiki_end: fsl_buffer_clear(&buf); if(rc) goto end; }/*WIKI*/ if( d->type==FSL_CATYPE_EVENT ){ fsl_buffer buf = fsl_buffer_empty; fsl_id_t tagid; fsl_id_t prior, subsequent; char zLength[40] = {0}; char const * zWiki; char const * zTag; fsl_size_t nWiki = 0; rc = fsl_buffer_appendf(&buf, "event-%s", d->E.uuid); if(rc) goto wiki_end; zTag = fsl_buffer_cstr(&buf); tagid = fsl_repo_tag_id( f, zTag, 1 ); if(tagid<=0){ rc = f->error.code ? f->error.code : fsl_cx_err_set(f, FSL_RC_RANGE, "Got unexpected RID (%"FSL_ID_T_PFMT") " "for tag [%s].", tagid, zTag); goto event_end; } zWiki = d->W.used ? fsl_buffer_cstr(&d->W) : ""; while( *zWiki && fsl_isspace(*zWiki) ){ ++zWiki; /* Historical behaviour: strip leading spaces. */ } nWiki = fsl_strlen(zWiki); fsl_snprintf( zLength, sizeof(zLength), "%"FSL_SIZE_T_PFMT, nWiki); rc = fsl_repo_tag_insert(f, FSL_TAGTYPE_ADD, zTag, zLength, rid, d->D, rid, NULL ); if(rc) goto event_end; prior = fsl_db_g_id(db, 0, "SELECT rid FROM tagxref" " WHERE tagid=%"FSL_ID_T_PFMT " AND mtime<%"FSL_JULIAN_T_PFMT" AND rid!=%"FSL_ID_T_PFMT " ORDER BY mtime DESC", tagid, d->D, rid); if(prior<0){ assert(db->error.code); rc = fsl_cx_uplift_db_error(f, db); goto event_end; } subsequent = fsl_db_g_id(db, 0, "SELECT rid FROM tagxref" " WHERE tagid=%"FSL_ID_T_PFMT " AND mtime>=%"FSL_JULIAN_T_PFMT " AND rid!=%"FSL_ID_T_PFMT " ORDER BY mtime", tagid, d->D, rid); if(subsequent<0){ assert(db->error.code); rc = fsl_cx_uplift_db_error(f, db); goto event_end; } else if( prior > 0 ){ rc = fsl_content_deltify(f, prior, rid, 0); if( !rc && !subsequent ){ rc = fsl_db_exec(db, "DELETE FROM event" " WHERE type='e'" " AND tagid=%"FSL_ID_T_PFMT " AND objid IN" " (SELECT rid FROM tagxref " " WHERE tagid=%"FSL_ID_T_PFMT")", tagid, tagid); } } if(rc) goto event_end; if( subsequent>0 ){ rc = fsl_content_deltify(f, rid, subsequent, 0); }else{ rc = fsl_db_exec(db, "REPLACE INTO event(" "type,mtime," "objid,tagid," "user,comment,bgcolor" ")VALUES(" "'e',%"FSL_JULIAN_T_PFMT"," "%"FSL_ID_T_PFMT",%"FSL_ID_T_PFMT"," "%B,%B," " (SELECT value FROM tagxref WHERE " " tagid=%d" " AND rid=%"FSL_ID_T_PFMT")" ");", d->E.julian, rid, tagid, &d->U, &d->C, FSL_TAGID_BGCOLOR, rid ); } event_end: fsl_buffer_clear(&buf); if(rc) goto end; }/*EVENT*/ else if( d->type==FSL_CATYPE_TICKET ){ /* TODO: huge block from manifest_crosslink(). A full port requires other infrastructure for collapsing relatively close time values into the same time for timeline purposes. i'd prefer to wait for the crosslink callback mechanism and do this there. */ }/*TICKET*/ if(rc) goto end; if( d->type==FSL_CATYPE_CONTROL && !(FSL_CX_F_MF_TAGS_NO_EVENT & f->flags)){ /* Create timeline event entry for all tags in this control construct. Note that we are using a lot of historical code which hard-codes english-lanuage text and links which only work in fossil(1). i would prefer to farm this out to a callback which gets passed a const handle to the deck, and provide an implementation which mimics fossil(1). */ fsl_buffer comment = fsl_buffer_empty; fsl_size_t i; const char *zName; const char *zValue; const char *zUuid; int branchMove = 0; int const uuidLen = 8; fsl_card_T const * tag = NULL; fsl_card_T const * prevTag = NULL; fsl_list const * li = &d->T;; for( i = 0; !rc && (i < li->used); ++i, prevTag = tag){ fsl_card_T const * tag = (fsl_card_T const *)li->list[i]; char isProp = 0, isAdd = 0, isCancel = 0; zUuid = tag->uuid; if( i==0 || 0!=fsl_uuidcmp(tag->uuid, prevTag->uuid)){ if( i>0 ) { rc = fsl_buffer_append(&comment, " ", 1); } if(!rc) rc = fsl_buffer_appendf(&comment, "Edit [%.*s]:", uuidLen, zUuid); branchMove = 0; } if(rc) goto end_ctl; isProp = FSL_TAGTYPE_PROPAGATING==tag->type; isAdd = FSL_TAGTYPE_ADD==tag->type; isCancel = FSL_TAGTYPE_CANCEL==tag->type; assert(isProp || isAdd || isCancel); zName = fsl_buffer_cstr(&tag->name); zValue = fsl_buffer_cstr(&tag->value); if( isProp && 0==fsl_strcmp(zName, "branch")){ rc = fsl_buffer_appendf(&comment, " Move to branch %s" "[/timeline?r=%h&nd&dp=%.*s | %h].", zValue, uuidLen, zUuid, zValue); branchMove = 1; }else if( isProp && fsl_strcmp(zName, "bgcolor")==0 ){ rc = fsl_buffer_appendf(&comment, " Change branch background color to \"%h\".", zValue); }else if( isAdd && fsl_strcmp(zName, "bgcolor")==0 ){ rc = fsl_buffer_appendf(&comment, " Change background color to \"%h\".", zValue); }else if( isCancel && fsl_strcmp(zName, "bgcolor")==0 ){ rc = fsl_buffer_appendf(&comment, " Cancel background color."); }else if( isAdd && fsl_strcmp(zName, "comment")==0 ){ rc = fsl_buffer_appendf(&comment, " Edit check-in comment."); }else if( isAdd && fsl_strcmp(zName, "user")==0 ){ rc = fsl_buffer_appendf(&comment, " Change user to \"%h\".", zValue); }else if( isAdd && fsl_strcmp(zName, "date")==0 ){ rc = fsl_buffer_appendf(&comment, " Timestamp %h.", zValue); }else if( isCancel && memcmp(zName, "sym-",4)==0 ){ if( !branchMove ){ rc = fsl_buffer_appendf(&comment, " Cancel tag %h.", zName+4); } }else if( isProp && memcmp(zName, "sym-",4)==0 ){ if( !branchMove ){ rc = fsl_buffer_appendf(&comment, " Add propagating tag \"%h\".", zName+4); } }else if( isAdd && memcmp(zName, "sym-",4)==0 ){ rc = fsl_buffer_appendf(&comment, " Add tag \"%h\".", zName+4); }else if( isCancel && memcmp(zName, "sym-",4)==0 ){ rc = fsl_buffer_appendf(&comment, " Cancel tag \"%h\".", zName+4); }else if( isAdd && fsl_strcmp(zName, "closed")==0 ){ rc = fsl_buffer_append(&comment, " Marked \"Closed\"", -1); if( !rc && zValue && *zValue ){ rc = fsl_buffer_appendf(&comment, " with note \"%h\"", zValue); } if(!rc) rc = fsl_buffer_append(&comment, ".", 1); }else if( isCancel && fsl_strcmp(zName, "closed")==0 ){ rc = fsl_buffer_append(&comment, " Removed the \"Closed\" mark", -1); if( !rc && zValue && *zValue ){ rc = fsl_buffer_appendf(&comment, " with note \"%h\"", zValue); } if(!rc) rc = fsl_buffer_append(&comment, ".", 1); }else { if( isCancel ){ rc = fsl_buffer_appendf(&comment, " Cancel \"%h\"", zName); }else if( isAdd ){ rc = fsl_buffer_appendf(&comment, " Add \"%h\"", zName); }else{ assert(isProp); rc = fsl_buffer_appendf(&comment, " Add propagating \"%h\"", zName); } if(rc) goto end_ctl; if( zValue && zValue[0] ){ rc = fsl_buffer_appendf(&comment, " with value \"%h\".", zValue); }else{ rc = fsl_buffer_appendf(&comment, "."); } } } /* foreach tag loop */ if(!rc){ rc = fsl_db_exec(db, "REPLACE INTO event(type,mtime,objid,user,comment)" "VALUES('g',%"FSL_JULIAN_T_PFMT",%"FSL_ID_T_PFMT",%B,%B)", mtime, rid, &d->U, &comment); } end_ctl: fsl_buffer_clear(&comment); if(rc) goto end; }/*CONTROL*/ assert(!rc); /* Call any crosslink callbacks... */ if(f->xlinkers.list){ int i; for( i = 0; !rc && (i < f->xlinkers.used); ++i ){ fsl_xlinker * xl = f->xlinkers.list+i; rc = xl->f( f, d, xl->state ); } if(rc){ if(!f->error.code){ fsl_cx_err_set(f, rc, "Crosslink callback handler " "#%d failed with code %d (%s).", i, rc, fsl_rc_cstr(rc)); } goto end; } } end: if(!rc){ rc = fsl_db_transaction_end(db, 0); }else{ if(db->error.code && !f->error.code){ fsl_cx_uplift_db_error(f,db); } fsl_db_transaction_end(db, 1); } return rc; } /** ** Return true if z points to the first character after a blank line. ** Tolerate either \r\n or \n line endings. */ static char fsl_after_blank_line(const char *z){ if( z[-1]!='\n' ) return 0; if( z[-2]=='\n' ) return 1; if( z[-2]=='\r' && z[-3]=='\n' ) return 1; return 0; } /** ** Verifies that ca points to at least 35 bytes of memory ** which hold (at the end) a Z card and its hash value. ** ** Returns 0 if the string does not contain a Z card, ** a positive value if it can validate the Z card's hash, ** and a negative value on hash mismatch. */ char fsl_mf_verify_Z_card(unsigned char const * ca, fsl_size_t n){ /* fsl_md5_cx md5 = fsl_md5_cx_empty; */ if( n<35 ) return 0; if( ca[n-35]!='Z' || ca[n-34]!=' ' ) return 0; else{ unsigned char digest[16]; char hex[FSL_MD5_STRLEN+1]; unsigned char const * zHash = ca+n-FSL_MD5_STRLEN-1; fsl_md5_cx md5 = fsl_md5_cx_empty; unsigned char const * zHashEnd = ca + n - 2 /* 'F ' */ - FSL_MD5_STRLEN - 1 /* \n */; assert( 'Z' == (char)*zHashEnd ); fsl_md5_update(&md5, ca, zHashEnd-ca); fsl_md5_final(&md5, digest); fsl_md5_digest_to_base16(digest, hex); return (0==memcmp(zHash, hex, FSL_MD5_STRLEN)) ? 1 : -1; } } void fsl_remove_pgp_signature(unsigned char const **pz, fsl_size_t *pn){ unsigned char const *z = *pz; fsl_int_t n = (fsl_int_t)*pn; fsl_int_t i; if( memcmp(z, "-----BEGIN PGP SIGNED MESSAGE-----", 34)!=0 ) return; for(i=34; i=n ) return; z += i; n -= i; *pz = z; for(i=n-1; i>=0; i--){ if( z[i]=='\n' && memcmp(&z[i],"\n-----BEGIN PGP SIGNATURE-", 25)==0 ){ n = i+1; break; } } *pn = (fsl_size_t)n; return; } /** ** Internal helper for parsing manifests. */ struct fsl_src { /** ** First char of the next token. */ unsigned char * z; /** ** One-past-the-end of the manifest. */ unsigned char * zEnd; /** ** True if z points to the start of a new line. */ char atEol; }; typedef struct fsl_src fsl_src; static const fsl_src fsl_src_empty = {NULL,NULL,0}; /** ** Return a pointer to the next token. The token is zero-terminated. ** Return NULL if there are no more tokens on the current line. If ** pLen is not NULL and this function returns non-NULL then *pLen is ** set to the byte length of the new token. */ static unsigned char *fsl_mf_next_token(fsl_src *p, fsl_size_t *pLen){ unsigned char *z; unsigned char *zStart; int c; if( p->atEol ) return NULL; zStart = z = p->z; while( (c=(*z))!=' ' && c!='\n' ){ ++z; } *z = 0; p->z = &z[1]; p->atEol = c=='\n'; if( pLen ) *pLen = z - zStart; return zStart; } /** ** Return the card-type for the next card. Or, return 0 if there are no ** more cards or if we are not at the end of the current card. */ static unsigned char mf_next_card(fsl_src *p){ unsigned char c; if( !p->atEol || p->z>=p->zEnd ) return 0; c = p->z[0]; if( p->z[1]==' ' ){ p->z += 2; p->atEol = 0; }else if( p->z[1]=='\n' ){ p->z += 2; p->atEol = 1; }else{ c = 0; } return c; } /** ** Internal helper for fsl_deck_parse(). Expects l to be an array of ** 26 entries, representing the letters of the alphabet (A-Z), ** with a value of 0 if the card was not seen during parsing ** and a value >0 if it was. Returns the deduced artifact type. ** ** It should guess right for any legal manifests, but it does not go ** out of its way to detect incomplete/invalid ones. */ static fsl_catype_t fsl_mf_guess_type( int * l ){ #if 0 int i; assert(!l[26]); MARKER(("Cards seen during parse:\n")); for( i = 0; i < 26; ++i ){ if(l[i]) putchar('A'+i); } putchar('\n'); #endif /* Now look for combinations of cards which will uniquely identify any syntactical legal combination of cards. A larger brain than mine could probably come up with a hash of l[] which could determine this in O(1). But please don't reimplement this as such unless mere mortals can maintain it - any performance gain is insignificant in the context of the underlying SCM/db operations. */ #define L(X) l[X-'A'] if(L('M')) return FSL_CATYPE_CLUSTER; else if(L('E')) return FSL_CATYPE_EVENT; else if(L('L') || L('W')) return FSL_CATYPE_WIKI; else if(L('J') || L('K')) return FSL_CATYPE_TICKET; else if(L('A')) return FSL_CATYPE_ATTACHMENT; else if(L('B') || L('F') || L('P') || L('Q') || L('R')) return FSL_CATYPE_MANIFEST; else if(L('D') && L('T') && L('U')) return FSL_CATYPE_CONTROL; #undef L return FSL_CATYPE_ANY; } int fsl_deck_parse(fsl_cx * f, fsl_id_t rid, fsl_deck * d, fsl_buffer * src){ #define ERROR(RC,MSG) do{ rc = (RC); zMsg = (MSG); goto bailout; } while(0) #define SYNTAX(MSG) ERROR(rc ? rc : FSL_RC_CA_SYNTAX,MSG) char isRepeat; int rc = 0; fsl_src x = fsl_src_empty; char const * zMsg = NULL; fsl_id_bag * seen; char cType = 0, cPrevType = 0; unsigned char * z = src ? src->mem : NULL; fsl_size_t tokLen = 0; unsigned char * token; fsl_size_t n = z ? src->used : 0; unsigned char * uuid; fsl_double_t ts; int cardCount = 0; fsl_buffer selfHash = fsl_buffer_empty; fsl_db * db = fsl_cx_db(f) /* We only need the db for converting date strings to doubles for julian. Since we have it, we also use it to populate d->B.uuid and d->rid. */; /* lettersSeen keeps track of the card letters we have seen so that we can then relatively quickly figure out what type of manifest we have parsed without having to inspect the card contents. We use an int (instead of char) so we can keep track of how many of each card we've seen. */ int lettersSeen[27] = {0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0}; if(!f || !d || !z) return FSL_RC_MISUSE; else if(!db) { return fsl_cx_err_set(f, FSL_RC_MISUSE, "Manifest parsing requires an opened " "db handle (any one will do)."); } /* Reminder: i'm leaving out v1's manifest cache until we can read manifests (which need to do be able to do in order to cache them). */ /* Every control artifact ends with a '\n' character. Exit early ** if that is not the case for this artifact. */ else if(!*z || !n || ( '\n' != z[n-1]) ){ return fsl_cx_err_set(f, FSL_RC_CA_SYNTAX, "%s.", n ? "Not terminated with \\n" : "Zero-length input"); } seen = &f->cache.mfSeen; if(rid<0){ return fsl_cx_err_set(f, FSL_RC_RANGE, "Invalid (negative) RID %"FSL_ID_T_PFMT " for manifest parse", rid); } else if((0==rid) || fsl_id_bag_contains(seen,rid)){ isRepeat = 1; }else{ isRepeat = 0; rc = fsl_id_bag_insert(seen, rid); if(rc){ assert(FSL_RC_OOM==rc); return rc; } } rc = fsl_sha1sum_buffer(src, &selfHash); if(rc) return rc; /* legacy: not yet clear if we need this: if( !isRepeat ) g.parseCnt[0]++; */ /* Strip off the PGP signature if there is one. Reminder to self: example of signed manifest: http://fossil-scm.org/index.html/artifact/28987096ac */ { unsigned char const * zz = z; fsl_remove_pgp_signature(&zz, &n); z = (unsigned char *)zz; } /* ** Verify that the first few characters of the artifact look like a ** control artifact. */ if( n<10 || z[0]<'A' || z[0]>'Z' || z[1]!=' ' ){ ERROR(FSL_RC_CA_SYNTAX, "Content does not look like a control artifact"); } /* Verify the Z card */ if( fsl_mf_verify_Z_card(z, n) < 0 ){ ERROR(FSL_RC_CONSISTENCY, "Z-card checksum mismatch"); } /* Reminder: parsing modifies the input (to simplify the tokenization/parsing). A side-effect of our OO API is that we potentially end up strdup'ing much of this memory into d->CARD. What we could do (and what i think v1 does?) is instead bypass the OO bits and assign all the pointers back into this modified copy, which we would then give over to the manifest (and save potentially tons of mallocs). IFF manifest parsing becomes prohibitively memory expensive, that would be the first big optimization to make, but it _would_ have side-effects and change the way a manifest and its constituent parts are cleaned up. We might want to make separate input and output manifest classes, with the OO API for output manifests (created by code) and input manifests optimized for read-only use and not duplicating the strings we dupe here. */ fsl_deck_clean(d); fsl_deck_init(f, d, FSL_CATYPE_ANY); /* Now parse, card by card... */ x.z = z; x.zEnd = z+n; x.atEol= 1; /* Parsing helpers... */ #define TOKEN(DEFOS) tokLen=0; token = fsl_mf_next_token(&x,&tokLen); \ if(token && tokLen && (DEFOS)) fsl_bytes_defossilize(token, &tokLen) #define TOKEN_EXISTS(MSG_IF_NOT) if(!token){ SYNTAX(MSG_IF_NOT); }(void)0 #define TOKEN_CHECKHEX(LEN,MSG) if(token \ && ((LEN)!=tokLen || \ !fsl_validate16((char const *)token,(LEN)))){ \ SYNTAX(MSG); } #define TOKEN_UUID(CARD) TOKEN_CHECKHEX(FSL_UUID_STRLEN,"Malformed UUID in " #CARD "-card") /** Reminder: we do not know the type of the manifest at this point, so all of the fsl_deck_add/set() bits below can't do their validation. We have to determine at parse-time (or afterwards) which type of deck it is based on the cards we've seen. */ #define SEEN(CARD) lettersSeen[*#CARD - 'A'] for( cPrevType=1; !rc && (0 < (cType = mf_next_card(&x))); cPrevType = cType, ++cardCount ){ if(cType='A' && cType<='Z'); if(cType>='A' && cType<='Z'){ ++lettersSeen[cType-'A']; }else{ SYNTAX("Invalid card name"); } if(FSL_CATYPE_ANY==d->type){ /* See if we can guess the type now. */ d->type = fsl_mf_guess_type(lettersSeen); if(FSL_CATYPE_ANY!=d->type){ assert(FSL_CATYPE_INVALID!=d->type); #if 0 MARKER(("Guessed manifest type on try #%d: %s\n", cardCount+1, fsl_catype_cstr(d->type))); #endif } }else{ /* If we know the deck type, make sure this card fits. */ if( !fsl_deck_check_type(d, cType) ){ rc = FSL_RC_TYPE; goto bailout; } } switch(cType){ /* ** A ?? ** ** Identifies an attachment to either a wiki page or a ticket. ** is the artifact that is the attachment. ** is omitted to delete an attachment. is the name of ** a wiki page or ticket to which that attachment is connected. */ case 'A':{ unsigned char * name, * src; if(1 ** ** A B-line gives the UUID for the baseline of a delta-manifest. */ case 'B':{ if(d->B.uuid){ SYNTAX("Multiple B-cards"); } TOKEN(0); TOKEN_UUID(B); rc = fsl_deck_B_set(d, (char const *)token); break; } /* ** C ** ** Comment text is fossil-encoded. There may be no more than ** one C line. C lines are required for manifests and are ** disallowed on all other control files. */ case 'C':{ if( d->C.used ){ SYNTAX("more than one C-card"); } TOKEN(1); TOKEN_EXISTS("Missing comment text for C-card"); rc = fsl_deck_C_set(d, (char const *)token, (fsl_int_t)tokLen); break; } /* ** D ** ** The timestamp should be ISO 8601. YYYY-MM-DDtHH:MM:SS ** There can be no more than 1 D line. D lines are required ** for all control files except for clusters. */ case 'D':{ #define TOKEN_DATETIME(LETTER,MEMBER) \ if( d->MEMBER>0.0 ) { SYNTAX("More than one "#LETTER"-card"); } \ TOKEN(0); \ TOKEN_EXISTS("Missing date part of "#LETTER"-card"); \ if(!fsl_str_is_date((char const *)token)){\ SYNTAX("Malformed date part of "#LETTER"-card"); \ } \ ts = fsl_db_string_to_julian(db, (char const *)token); \ if( ts<=0.0 ){ \ SYNTAX("Cannot parse date from "#LETTER"-card"); \ } (void)0 TOKEN_DATETIME(D,D); rc = fsl_deck_D_set(d, ts); break; } /* ** E ** ** An "event" card that contains the timestamp of the event in the ** format YYYY-MM-DDtHH:MM:SS and a unique identifier for the event. ** The event timestamp is distinct from the D timestamp. The D ** timestamp is when the artifact was created whereas the E timestamp ** is when the specific event is said to occur. */ case 'E':{ TOKEN_DATETIME(E,E.julian); TOKEN(0); TOKEN_EXISTS("Missing UUID part of E-card"); TOKEN_UUID(E); rc = fsl_deck_E_set(d, ts, (char const *)token); break; } /* ** F ?? ?? ?? ** ** Identifies a file in a manifest. Multiple F lines are ** allowed in a manifest. F lines are not allowed in any ** other control file. The filename and old-name are fossil-encoded. */ case 'F':{ char const * name; char const * perms = NULL; char const * oldName = NULL; int permI = 0; TOKEN(0); TOKEN_EXISTS("Missing name for F-card"); name = (char const *)token; TOKEN(0); TOKEN_UUID(F); uuid = token; if(uuid){ TOKEN(0); if(token){ perms = (char const *)token; if('w'==*perms) permI = FSL_MF_PERM_REGULAR; else if('x'==*perms) permI = FSL_MF_PERM_EXE; else if('l'==*perms) permI = FSL_MF_PERM_LINK; else{ assert(!"Unmatched perms string character!"); ERROR(FSL_RC_ERROR,"Internal error: unmatched perms string character"); } TOKEN(0); if(token) oldName = (char const *)token; } } rc = fsl_deck_F_add(d, name, (fsl_uuid_cstr)uuid, permI, oldName); break; } /* ** J ?? ** ** Specifies a name value pair for ticket. If the first character ** of is "+" then the is appended to any preexisting ** value. If is omitted then it is understood to be an ** empty string. */ case 'J':{ char const * field; char isAppend = 0; TOKEN(1); TOKEN_EXISTS("Missing field name for J-card"); field = (char const *)token; if('+'==*field){ isAppend = 1; ++field; } TOKEN(1); rc = fsl_deck_J_add(d, isAppend, field, (char const *)token); break; } /* ** K ** ** A K-line gives the UUID for the ticket which this control file ** is amending. */ case 'K':{ if(d->K){ SYNTAX("Multiple K-cards"); } TOKEN(0); TOKEN_EXISTS("Missing UUID in K-card"); TOKEN_UUID(K); rc = fsl_deck_K_set(d, (char const *)token); break; } /* ** L ** ** The wiki page title is fossil-encoded. There may be no more than ** one L line. */ case 'L':{ if(d->L.used){ SYNTAX("Multiple L-cards"); } TOKEN(1); TOKEN_EXISTS("Missing text for L-card"); rc = fsl_deck_L_set(d, (char const *)token, (fsl_int_t)tokLen); break; } /* ** M ** ** An M-line identifies another artifact by its UUID. M-lines ** occur in clusters only. */ case 'M':{ TOKEN(0); TOKEN_EXISTS("Missing UUID for M-card"); TOKEN_UUID(M); rc = fsl_deck_M_add(d, (char const *)token); break; } /* ** N ** ** An N-line identifies the mimetype of wiki or comment text. */ case 'N':{ if(1 ... ** ** Specify one or more other artifacts which are the parents of ** this artifact. The first parent is the primary parent. All ** others are parents by merge. */ case 'P':{ if(1 ?? ** ** Specify one or a range of checkins that are cherrypicked into ** this checkin ("+") or backed out of this checkin ("-"). */ case 'Q':{ char prefix; TOKEN(0); TOKEN_EXISTS("Missing target UUID for Q-card"); prefix = (char)*token; if('-'!=prefix && '+'!=prefix){ SYNTAX("Malformed target UUID in Q-card"); } uuid = ++token; --tokLen; TOKEN_UUID(Q); TOKEN(0); if(token){ TOKEN_UUID(Q); } rc = fsl_deck_Q_add(d, prefix, (char const *)uuid, (char const *)token); break; } /* ** R ** ** Specify the MD5 checksum over the name and content of all files ** in the manifest. */ case 'R':{ TOKEN(0); TOKEN_EXISTS("Missing MD5 token in R-card"); TOKEN_CHECKHEX(FSL_MD5_STRLEN,"Malformed MD5 token in R-card"); rc = fsl_deck_R_set(d, (char const *)token); break; } /* ** T (+|*|-) ?? ** ** Create or cancel a tag or property. The tagname is fossil-encoded. ** The first character of the name must be either "+" to create a ** singleton tag, "*" to create a propagating tag, or "-" to create ** anti-tag that undoes a prior "+" or blocks propagation of of ** a "*". ** ** The tag is applied to . If is "*" then the tag is ** applied to the current manifest. If is provided then ** the tag is really a property with the given value. ** ** Tags are not allowed in clusters. Multiple T lines are allowed. */ case 'T':{ unsigned char * name, * value; fsl_tag_type tagType = FSL_TAGTYPE_INVALID; TOKEN(0); TOKEN_EXISTS("Missing name for T-card"); name = token; TOKEN(0); TOKEN_EXISTS("Missing UUID on T-card"); if(FSL_UUID_STRLEN==tokLen){ TOKEN_UUID(T); /* A valid UUID */ uuid = token; }else if( 1==tokLen && '*'==(char)*token ){ uuid = NULL /* tag for the current artifact */; }else{ SYNTAX("Malformed UUID in T-card"); } TOKEN(1); value = token; switch(*name){ case '*': tagType = FSL_TAGTYPE_PROPAGATING; break; case '+': tagType = FSL_TAGTYPE_ADD; break; case '-': tagType = FSL_TAGTYPE_CANCEL; break; default: SYNTAX("Malformed tag name"); } ++name /* skip type marker byte */; /* Potential todo: add the order check from this commit: http://fossil-scm.org/index.html/info/55cacfcace */ rc = fsl_deck_T_add(d, tagType, (fsl_uuid_cstr)uuid, (char const *)name, (char const *)value); break; } /* ** U ?? ** ** Identify the user who created this control file by their ** login. Only one U line is allowed. Prohibited in clusters. ** If the user name is omitted, take that to be "anonymous". */ case 'U':{ if(d->U.used) SYNTAX("More than one U-card"); TOKEN(1); if(token){ rc = fsl_deck_U_set( d, (char const *)token, (fsl_int_t)tokLen ); }else{ rc = fsl_deck_U_set( d, "anonymous", 9); } break; } /* ** W ** ** The next bytes of the file contain the text of the wiki ** page. There is always an extra \n before the start of the next ** record. */ case 'W':{ fsl_size_t wlen; if(d->W.used){ SYNTAX("More than one W-card"); } TOKEN(0); TOKEN_EXISTS("Missing size token for W-card"); wlen = fsl_str_to_size((char const *)token); if(-1==wlen){ ERROR(FSL_RC_RANGE,"Wiki size token is invalid"); } if( (&x.z[wlen+1]) > x.zEnd){ SYNTAX("Not enough content after W-card"); } rc = fsl_buffer_append(&d->W, x.z, wlen); if(rc) goto bailout; x.z += wlen; if( '\n' != x.z[0] ){ SYNTAX("W-card content not \\n terminated"); } x.z[0] = 0; ++x.z; break; } /* ** Z ** ** MD5 checksum on this control file. The checksum is over all ** lines (other than PGP-signature lines) prior to the current ** line. This must be the last record. ** ** This card is required for all control file types except for ** Manifest. It is not required for manifest only for historical ** compatibility reasons. */ case 'Z':{ /* Heh? We validated the Z card first. We cannot compare against the original blob now because we've modified it. */ goto end; } default: rc = fsl_cx_err_set(f, FSL_RC_CA_SYNTAX, "Unknown card '%c' in manifest", cType); goto bailout; }/*switch(cType)*/ }/* for-each-card */ #if 1 /* Remove these when we are done porting resp. we can avoid these unused-var warnings. */ if(isRepeat){} #endif end: if(FSL_CATYPE_ANY==d->type){ rc = fsl_cx_err_set(f, FSL_RC_ERROR, "Internal error: could not determine type of " "control artifact we just (successfully!) " "parsed."); goto bailout; }else { /* Make sure we didn't pick up any cards which were picked up before d->type was guessed and are invalid for the post-guessed type. */ int i = 0; for( ; i < 27; ++i ){ if((lettersSeen[i]>0) && !fsl_card_is_legal(d->type, 'A'+i )){ rc = fsl_cx_err_set(f, FSL_RC_CA_SYNTAX, "Determined during post-parse processing that " "the parsed deck (type %s) contains an illegal " "card type (%c).", fsl_catype_cstr(d->type), 'A'+i); goto bailout; } } } assert(FSL_CATYPE_MANIFEST==d->type || FSL_CATYPE_CLUSTER==d->type || FSL_CATYPE_CONTROL==d->type || FSL_CATYPE_WIKI==d->type || FSL_CATYPE_TICKET==d->type || FSL_CATYPE_ATTACHMENT==d->type || FSL_CATYPE_EVENT==d->type); assert(0==rc); d->rid = fsl_repo_uuid_to_rid(f, fsl_buffer_cstr(&selfHash)); /* assert(d->rid>0); can't run test manifest through this if we assert here. */ if(d->rid<0){ assert(f->error.code); goto bailout; } d->uuid = fsl_buffer_str(&selfHash) /* transfer ownership */; return 0; bailout: assert(0 != rc); fsl_buffer_clear(&selfHash); if(zMsg){ rc = fsl_cx_err_set(f, rc, "%s", zMsg); }else if(f->error.code){ /* We already have error state. Do nothing. */ }else if(d->error.code){ /* Uplift this into f... */ fsl_cx_err_set_e(f, &d->error); }else if(db->error.code){ /* Uplift this into f... */ fsl_cx_uplift_db_error(f, db); } return rc; #undef SEEN #undef TOKEN_DATETIME #undef SYNTAX #undef TOKEN_CHECKHEX #undef TOKEN_EXISTS #undef TOKEN_UUID #undef TOKEN #undef ERROR } int fsl_deck_load_rid( fsl_cx * f, fsl_id_t rid, fsl_deck * d, fsl_catype_t type ){ fsl_buffer buf = fsl_buffer_empty; int rc; if(!f || !d) return FSL_RC_CA_SYNTAX; else if(rid<=0){ return fsl_cx_err_set(f, FSL_RC_RANGE, "Invalid RID for %s(): %"FSL_ID_T_PFMT, __func__, rid); } rc = fsl_content_get(f, rid, &buf); if(!rc){ rc = fsl_deck_parse(f, rid, d, &buf); if(!rc && ( type!=FSL_CATYPE_ANY && d->type!=type )){ rc = fsl_cx_err_set(f, FSL_RC_TYPE, "RID %"FSL_ID_T_PFMT" is of type %s, " "but the caller requested type %s.", rid, fsl_catype_cstr(d->type), fsl_catype_cstr(type)); } } fsl_buffer_clear(&buf); return rc; } static int fsl_deck_baseline_load( fsl_deck * d ){ int rc = 0; fsl_deck bl = fsl_deck_empty; fsl_id_t rid; fsl_cx * f; fsl_db * db; f = d ? d->f : NULL; db = d ? fsl_cx_db_repo(d->f) : NULL; assert(d->f); assert(d); if(!d || !d->f) return FSL_RC_MISUSE; else if(d->B.baseline || !d->B.uuid) return 0; else if(!db){ return fsl_cx_err_set(f, FSL_RC_NOT_A_REPO, "Fossil has no repo db opened."); } else if(d->rid<=0){ return fsl_cx_err_set(f, FSL_RC_RANGE, "%s(): fsl_deck::rid is not set.", __func__); } rid = fsl_repo_uuid_to_rid(f, d->B.uuid); if(rid<0) return FSL_RC_ERROR; else if(!rid){ fsl_db_exec(db, "INSERT OR IGNORE INTO orphan(rid, baseline) " "VALUES(%"FSL_ID_T_PFMT",%"FSL_ID_T_PFMT")", d->rid, rid); rc = fsl_cx_err_set(f, FSL_RC_RANGE, "Could not find/load baseline manifest [%s], " "parent of manifest rid #%"FSL_ID_T_PFMT".", d->B.uuid, d->rid); }else{ rc = fsl_deck_load_rid(f, rid, &bl, FSL_CATYPE_MANIFEST); if(!rc){ d->B.baseline = fsl_deck_malloc(); if(!d->B.baseline){ fsl_deck_clean(&bl); rc = FSL_RC_OOM; }else{ void const * allocStampKludge = d->B.baseline->allocStamp; *d->B.baseline = bl /* Transfer ownership */; d->B.baseline->allocStamp = allocStampKludge /* But we need this intact for deallocation to work */; assert(f==d->B.baseline->f); } }else{ /* bl might be partially populated */ fsl_deck_finalize(&bl); } } return rc; } int fsl_deck_baseline_fetch( fsl_deck * d ){ return (d && (d->B.baseline || !d->B.uuid)) ? 0 : fsl_deck_baseline_load(d); } int fsl_deck_F_rewind( fsl_deck * d ){ int rc = 0; assert(d->f); d->F.cursor = 0; assert(d->f); if(!rc && d->B.uuid){ rc = fsl_deck_baseline_fetch(d); if(!rc){ assert(d->B.baseline); d = d->B.baseline; d->F.cursor = 0; } } return rc; } int fsl_deck_F_next( fsl_deck * d, fsl_card_F const ** rv ){ assert(d); assert(d->f); assert(rv); #define FCARD(DECK,NDX) ((fsl_card_F const *)(DECK)->F.list.list[NDX]) *rv = NULL; if(!d->B.baseline){ /* Manifest p is a baseline-manifest. Just scan down the list ** of files. */ if(d->B.uuid){ return fsl_cx_err_set(d->f, FSL_RC_MISUSE, "Deck has a UUID (%.*s) but no baseline loaded. " "Load the baseline before calling %s().", 8, d->B.uuid, __func__) /* We "could" just load the baseline from here. */; } if( d->F.cursor < d->F.list.used ){ *rv = FCARD(d, d->F.cursor++); assert(*rv); } return 0; }else{ /* Manifest p is a delta-manifest. Scan the baseline but amend the ** file list in the baseline with changes described by p. */ fsl_deck *pB = d->B.baseline; int cmp; while(1){ if( pB->F.cursor >= pB->F.list.used ){ /* We have used all entries out of the baseline. Return the next ** entry from the delta. */ if( d->F.cursor < d->F.list.used ) *rv = FCARD(d, d->F.cursor++); break; }else if( d->F.cursor>=d->F.list.used ){ /* We have used all entries from the delta. Return the next ** entry from the baseline. */ if( pB->F.cursorF.list.used ) *rv = FCARD(pB, pB->F.cursor++); break; }else if( (cmp = fsl_strcmp(FCARD(pB,pB->F.cursor)->name, FCARD(d, d->F.cursor)->name)) < 0){ /* The next baseline entry comes before the next delta entry. ** So return the baseline entry. */ *rv = FCARD(pB, pB->F.cursor++); break; }else if( cmp>0 ){ /* The next delta entry comes before the next baseline ** entry so return the delta entry */ *rv = FCARD(d, d->F.cursor++); break; }else if( FCARD(d, d->F.cursor)->uuid ){ /* The next delta entry is a replacement for the next baseline ** entry. Skip the baseline entry and return the delta entry */ pB->F.cursor++; *rv = FCARD(d, d->F.cursor++); break; }else{ /* The next delta entry is a delete of the next baseline ** entry. Skip them both. Repeat the loop to find the next ** non-delete entry. */ pB->F.cursor++; d->F.cursor++; continue; } } return 0; } #undef FCARD } int fsl_deck_save( fsl_deck * d, char isPrivate ){ int rc; fsl_cx * f = d ? d->f : NULL; fsl_db * db = f ? fsl_cx_db_repo(f) : NULL; fsl_buffer buf = fsl_buffer_empty; fsl_id_t newRid = 0; char const oldPrivate = f ? f->cache.markPrivate : 0; char const isNew = d && !d->uuid; if(!f || !d ) return FSL_RC_MISUSE; else if(!db) return FSL_RC_NOT_A_REPO; else if( (d->rid>0 && !d->uuid) || (d->rid<=0 && d->uuid)){ return fsl_cx_err_set(f, FSL_RC_MISUSE, "The input deck must have either rid _and_ UUID " "or neither of rid/UUID. A mixture is ambiguous."); } rc = fsl_deck_unshuffle(d, d->F.list.used ? 1 : 0); if(rc) return rc; rc = fsl_deck_output(f, d, fsl_output_f_buffer, &buf); if(rc) return rc; rc = fsl_db_transaction_begin(db); if(rc){ fsl_buffer_clear(&buf); return rc; } /* Starting here, don't return, use (goto end) instead. */ f->cache.markPrivate = isPrivate; rc = fsl_content_put_ex(f, &buf, d->uuid, (d->rid>0) ? d->rid : 0, 0U, isPrivate, &newRid); if(rc) goto end; assert(newRid>0); rc = fsl_sha1sum_buffer( &buf, &buf ); if(rc) goto end; /* We need d->uuid and d->rid for crosslinking purposes, but will unset them on error (if we set them) because their values will no longer be in the db after rollback... */ if(d->uuid){ /* Compare original and resulting hashes. */ if(0 != fsl_strcmp(fsl_buffer_cstr(&buf), d->uuid)){ rc = fsl_cx_err_set(f, FSL_RC_CONSISTENCY, "Input deck's original UUID and resulting UUID " "do not match: [%s] vs [%b]", d->uuid, &buf); goto end; } /* ??? assert(d->rid==newRid); */ d->rid = newRid; }else{ fsl_free(d->uuid); d->uuid = fsl_buffer_str(&buf) /* transfer ownership */; buf = fsl_buffer_empty; d->rid = newRid; } if(isNew && (FSL_CATYPE_WIKI==d->type)){ /* Analog to v1's wiki.c:wiki_put(): */ /* MISSING: v1's wiki.c:wiki_put() handles the moderation bits. */ if(d->P.used){ fsl_id_t pid = fsl_deck_P_get_id(d, 0); assert(pid>0); rc = fsl_content_deltify(f, pid, d->rid, 0); if(rc) goto end; } rc = fsl_db_exec_multi(db, "INSERT OR IGNORE INTO unsent " "VALUES(%"FSL_ID_T_PFMT");" "INSERT OR IGNORE INTO unclustered " "VALUES(%"FSL_ID_T_PFMT");", d->rid, d->rid); if(rc){ fsl_cx_uplift_db_error(f, db); goto end; } } rc = fsl_deck_crosslink(f, d); if(rc) goto end; end: f->cache.markPrivate = oldPrivate; if(!rc) rc = fsl_db_transaction_end( db, 0); else fsl_db_transaction_end(db, 1); if(rc){ if(isNew){ fsl_free(d->uuid); d->uuid = NULL; d->rid = 0; } if(!f->error.code){ if(d->error.code){ rc = fsl_cx_err_set_e(f, &d->error); }else if(db->error.code){ rc = fsl_cx_uplift_db_error(f, db); } } } fsl_buffer_clear(&buf); return rc; } #undef MARKER