Fossil SCM

Enhance the manifest parser to support parsing of Forum posts artifacts. At the same time, simplify the artifact syntax error detection logic using tables rather than straight code.

drh 2018-07-19 21:31 forum-v2
Commit e893e9d01b8b217d8dccbf111e31e5e6fec5c1e62f7191c8b6661ada73c5519a
2 files changed +163 -79 +11
+163 -79
--- src/manifest.c
+++ src/manifest.c
@@ -34,10 +34,11 @@
3434
#define CFTYPE_CONTROL 3
3535
#define CFTYPE_WIKI 4
3636
#define CFTYPE_TICKET 5
3737
#define CFTYPE_ATTACHMENT 6
3838
#define CFTYPE_EVENT 7
39
+#define CFTYPE_FORUM 8
3940
4041
/*
4142
** File permissions used by Fossil internally.
4243
*/
4344
#define PERM_REG 0 /* regular file */
@@ -76,16 +77,19 @@
7677
char *zUser; /* Name of the user from the U card. */
7778
char *zRepoCksum; /* MD5 checksum of the baseline content. R card. */
7879
char *zWiki; /* Text of the wiki page. W card. */
7980
char *zWikiTitle; /* Name of the wiki page. L card. */
8081
char *zMimetype; /* Mime type of wiki or comment text. N card. */
82
+ char *zThreadTitle; /* The forum thread title. H card */
8183
double rEventDate; /* Date of an event. E card. */
8284
char *zEventId; /* Artifact hash for an event. E card. */
8385
char *zTicketUuid; /* UUID for a ticket. K card. */
8486
char *zAttachName; /* Filename of an attachment. A card. */
8587
char *zAttachSrc; /* Artifact hash for document being attached. A card. */
8688
char *zAttachTarget; /* Ticket or wiki that attachment applies to. A card */
89
+ char *zThreadRoot; /* Thread root artifact. G card */
90
+ char *zInReplyTo; /* Forum in-reply-to artifact. I card */
8791
int nFile; /* Number of F cards */
8892
int nFileAlloc; /* Slots allocated in aFile[] */
8993
int iFile; /* Index of current file in iterator */
9094
ManifestFile *aFile; /* One entry for each F-card */
9195
int nParent; /* Number of parents. */
@@ -112,10 +116,42 @@
112116
char *zName; /* Key or field name */
113117
char *zValue; /* Value of the field */
114118
} *aField; /* One for each J card */
115119
};
116120
#endif
121
+
122
+/*
123
+** Allowed and required card types in each style of artifact
124
+*/
125
+static struct {
126
+ const char *zAllowed; /* Allowed cards. Human-readable */
127
+ const char *zRequired; /* Required cards. Human-readable */
128
+} manifestCardTypes[] = {
129
+ /* Allowed Required */
130
+ /* CFTYPE_MANIFEST 1 */ { "BCDFNPQRTUZ", "CDUZ" },
131
+ /* CFTYPE_CLUSTER 2 */ { "MZ", "MZ" },
132
+ /* CFTYPE_CONTROL 3 */ { "DTUZ", "DTUZ" },
133
+ /* CFTYPE_WIKI 4 */ { "DLNPUWZ", "DLUWZ" },
134
+ /* CFTYPE_TICKET 5 */ { "DJKUZ", "DJKUZ" },
135
+ /* CFTYPE_ATTACHMENT 6 */ { "ACDNUZ", "ADZ" },
136
+ /* CFTYPE_EVENT 7 */ { "CDENPTUWZ", "DEWZ" },
137
+ /* CFTYPE_FORUM 8 */ { "DGHINPUWZ", "DUWZ" },
138
+};
139
+
140
+/*
141
+** Names of manifest types
142
+*/
143
+static const char *azNameOfMType[] = {
144
+ "manifest",
145
+ "cluster",
146
+ "tag",
147
+ "wiki",
148
+ "ticket",
149
+ "attachment",
150
+ "technote",
151
+ "forum post"
152
+};
117153
118154
/*
119155
** A cache of parsed manifests. This reduces the number of
120156
** calls to manifest_parse() when doing a rebuild.
121157
*/
@@ -147,10 +183,35 @@
147183
if( p->pBaseline ) manifest_destroy(p->pBaseline);
148184
memset(p, 0, sizeof(*p));
149185
fossil_free(p);
150186
}
151187
}
188
+
189
+/*
190
+** Given a string of upper-case letters, compute a mask of the letters
191
+** present. For example, "ABC" computes 0x0007. "DE" gives 0x0018".
192
+*/
193
+static unsigned int manifest_card_mask(const char *z){
194
+ unsigned int m = 0;
195
+ char c;
196
+ while( (c = *(z++))>='A' && c<='Z' ){
197
+ m |= 1 << (c - 'A');
198
+ }
199
+ return m;
200
+}
201
+
202
+/*
203
+** Given an integer mask representing letters A-Z, return the
204
+** letter which is the first bit set in the mask. Example:
205
+** 0x03520 gives 'F' since the F-bit is the lowest.
206
+*/
207
+static char maskToType(unsigned int x){
208
+ char c = 'A';
209
+ if( x==0 ) return '?';
210
+ while( (x&1)==0 ){ x >>= 1; c++; }
211
+ return c;
212
+}
152213
153214
/*
154215
** Add an element to the manifest cache using LRU replacement.
155216
*/
156217
void manifest_cache_insert(Manifest *p){
@@ -352,11 +413,10 @@
352413
** The card type determines the other parameters to the card.
353414
** Cards must occur in lexicographical order.
354415
*/
355416
Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){
356417
Manifest *p;
357
- int seenZ = 0;
358418
int i, lineNo=0;
359419
ManifestText x;
360420
char cPrevType = 0;
361421
char cType;
362422
char *z;
@@ -364,10 +424,13 @@
364424
char *zUuid;
365425
int sz = 0;
366426
int isRepeat, hasSelfRefTag = 0;
367427
static Bag seen;
368428
const char *zErr = 0;
429
+ unsigned int m;
430
+ unsigned int seenCard = 0; /* Which card types have been seen */
431
+ char zErrBuf[100]; /* Write error messages here */
369432
370433
if( rid==0 ){
371434
isRepeat = 1;
372435
}else if( bag_find(&seen, rid) ){
373436
isRepeat = 1;
@@ -422,10 +485,12 @@
422485
x.z = z;
423486
x.zEnd = &z[n];
424487
x.atEol = 1;
425488
while( (cType = next_card(&x))!=0 && cType>=cPrevType ){
426489
lineNo++;
490
+ if( cType<'A' || cType>'Z' ) SYNTAX("bad card type");
491
+ seenCard |= 1 << (cType-'A');
427492
switch( cType ){
428493
/*
429494
** A <filename> <target> ?<source>?
430495
**
431496
** Identifies an attachment to either a wiki page or a ticket.
@@ -454,10 +519,11 @@
454519
SYNTAX("invalid source on A-card");
455520
}
456521
p->zAttachName = (char*)file_tail(zName);
457522
p->zAttachSrc = zSrc;
458523
p->zAttachTarget = zTarget;
524
+ p->type = CFTYPE_ATTACHMENT;
459525
break;
460526
}
461527
462528
/*
463529
** B <uuid>
@@ -469,10 +535,11 @@
469535
p->zBaseline = next_token(&x, &sz);
470536
if( p->zBaseline==0 ) SYNTAX("missing hash on B-card");
471537
if( !hname_validate(p->zBaseline,sz) ){
472538
SYNTAX("invalid hash on B-card");
473539
}
540
+ p->type = CFTYPE_MANIFEST;
474541
break;
475542
}
476543
477544
478545
/*
@@ -520,10 +587,11 @@
520587
if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card");
521588
p->zEventId = next_token(&x, &sz);
522589
if( !hname_validate(p->zEventId, sz) ){
523590
SYNTAX("malformed hash on E-card");
524591
}
592
+ p->type = CFTYPE_EVENT;
525593
break;
526594
}
527595
528596
/*
529597
** F <filename> ?<uuid>? ?<permissions>? ?<old-name>?
@@ -565,10 +633,59 @@
565633
p->aFile[i].zPerm = zPerm;
566634
p->aFile[i].zPrior = zPriorName;
567635
if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){
568636
SYNTAX("incorrect F-card sort order");
569637
}
638
+ p->type = CFTYPE_MANIFEST;
639
+ break;
640
+ }
641
+
642
+ /*
643
+ ** G <hash>
644
+ **
645
+ ** A G-card identifies the initial root forum post for the thread
646
+ ** of which this post is a part. Forum posts only.
647
+ */
648
+ case 'G': {
649
+ if( p->zThreadRoot!=0 ) SYNTAX("more than one G-card");
650
+ p->zThreadRoot = next_token(&x, &sz);
651
+ if( p->zThreadRoot==0 ) SYNTAX("missing hash on G-card");
652
+ if( !hname_validate(p->zThreadRoot,sz) ){
653
+ SYNTAX("Invalid hash on G-card");
654
+ }
655
+ p->type = CFTYPE_FORUM;
656
+ break;
657
+ }
658
+
659
+ /*
660
+ ** H <threadtitle>
661
+ **
662
+ ** The title for a forum thread.
663
+ */
664
+ case 'H': {
665
+ if( p->zThreadTitle!=0 ) SYNTAX("more than one H-card");
666
+ p->zThreadTitle = next_token(&x,0);
667
+ if( p->zThreadTitle==0 ) SYNTAX("missing title on H-card");
668
+ defossilize(p->zThreadTitle);
669
+ p->type = CFTYPE_FORUM;
670
+ break;
671
+ }
672
+
673
+ /*
674
+ ** I <hash>
675
+ **
676
+ ** A I-card identifies another forum post that the current forum post
677
+ ** is in reply to.
678
+ */
679
+ case 'I': {
680
+ if( p->zInReplyTo!=0 ) SYNTAX("more than one I-card");
681
+ p->zInReplyTo = next_token(&x, &sz);
682
+ if( p->zInReplyTo==0 ) SYNTAX("missing hash on I-card");
683
+ if( !hname_validate(p->zInReplyTo,sz) ){
684
+ SYNTAX("Invalid hash on I-card");
685
+ }
686
+ p->type = CFTYPE_FORUM;
570687
break;
571688
}
572689
573690
/*
574691
** J <name> ?<value>?
@@ -594,10 +711,11 @@
594711
p->aField[i].zName = zName;
595712
p->aField[i].zValue = zValue;
596713
if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){
597714
SYNTAX("incorrect J-card sort order");
598715
}
716
+ p->type = CFTYPE_TICKET;
599717
break;
600718
}
601719
602720
603721
/*
@@ -611,10 +729,11 @@
611729
p->zTicketUuid = next_token(&x, &sz);
612730
if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size");
613731
if( !validate16(p->zTicketUuid, sz) ){
614732
SYNTAX("invalid K-card UUID");
615733
}
734
+ p->type = CFTYPE_TICKET;
616735
break;
617736
}
618737
619738
/*
620739
** L <wikititle>
@@ -628,10 +747,11 @@
628747
if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card");
629748
defossilize(p->zWikiTitle);
630749
if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){
631750
SYNTAX("L-card has malformed wiki name");
632751
}
752
+ p->type = CFTYPE_WIKI;
633753
break;
634754
}
635755
636756
/*
637757
** M <hash>
@@ -653,10 +773,11 @@
653773
i = p->nCChild++;
654774
p->azCChild[i] = zUuid;
655775
if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){
656776
SYNTAX("M-card in the wrong order");
657777
}
778
+ p->type = CFTYPE_CLUSTER;
658779
break;
659780
}
660781
661782
/*
662783
** N <uuid>
@@ -717,10 +838,11 @@
717838
p->aCherrypick[n].zCPTarget = zUuid;
718839
p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz);
719840
if( zUuid && !hname_validate(zUuid,sz) ){
720841
SYNTAX("invalid second hash on Q-card");
721842
}
843
+ p->type = CFTYPE_MANIFEST;
722844
break;
723845
}
724846
725847
/*
726848
** R <md5sum>
@@ -731,10 +853,11 @@
731853
case 'R': {
732854
if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card");
733855
p->zRepoCksum = next_token(&x, &sz);
734856
if( sz!=32 ) SYNTAX("wrong size cksum on R-card");
735857
if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum");
858
+ p->type = CFTYPE_MANIFEST;
736859
break;
737860
}
738861
739862
/*
740863
** T (+|*|-)<tagname> <uuid> ?<value>?
@@ -759,11 +882,11 @@
759882
if( zUuid==0 ) SYNTAX("missing artifact hash on T-card");
760883
zValue = next_token(&x, 0);
761884
if( zValue ) defossilize(zValue);
762885
if( hname_validate(zUuid, sz) ){
763886
/* A valid artifact hash */
764
- if( p->zEventId ) SYNTAX("non-self-referential T-card in event");
887
+ if( p->zEventId ) SYNTAX("non-self-referential T-card in technote");
765888
}else if( sz==1 && zUuid[0]=='*' ){
766889
zUuid = 0;
767890
hasSelfRefTag = 1;
768891
if( p->zEventId && zName[0]!='+' ){
769892
SYNTAX("propagating T-card in event");
@@ -858,104 +981,65 @@
858981
*/
859982
case 'Z': {
860983
zUuid = next_token(&x, &sz);
861984
if( sz!=32 ) SYNTAX("wrong size for Z-card cksum");
862985
if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum");
863
- seenZ = 1;
864986
break;
865987
}
866988
default: {
867989
SYNTAX("unrecognized card");
868990
}
869991
}
870992
}
871993
if( x.z<x.zEnd ) SYNTAX("extra characters at end of card");
872994
873
- if( p->nCChild>0 ){
874
- if( p->zAttachName
875
- || p->zBaseline
876
- || p->zComment
877
- || p->rDate>0.0
878
- || p->zEventId
879
- || p->nFile>0
880
- || p->nField>0
881
- || p->zTicketUuid
882
- || p->zWikiTitle
883
- || p->zMimetype
884
- || p->nParent>0
885
- || p->nCherrypick>0
886
- || p->zRepoCksum
887
- || p->nTag>0
888
- || p->zUser
889
- || p->zWiki
890
- ){
891
- SYNTAX("cluster contains a card other than M- or Z-");
892
- }
893
- if( !seenZ ) SYNTAX("missing Z-card on cluster");
894
- p->type = CFTYPE_CLUSTER;
895
- }else if( p->zEventId ){
896
- if( p->zAttachName ) SYNTAX("A-card in event");
897
- if( p->zBaseline ) SYNTAX("B-card in event");
898
- if( p->rDate<=0.0 ) SYNTAX("missing date on event");
899
- if( p->nFile>0 ) SYNTAX("F-card in event");
900
- if( p->nField>0 ) SYNTAX("J-card in event");
901
- if( p->zTicketUuid ) SYNTAX("K-card in event");
902
- if( p->zWikiTitle!=0 ) SYNTAX("L-card in event");
903
- if( p->zRepoCksum ) SYNTAX("R-card in event");
904
- if( p->zWiki==0 ) SYNTAX("missing W-card on event");
905
- if( !seenZ ) SYNTAX("missing Z-card on event");
906
- p->type = CFTYPE_EVENT;
907
- }else if( p->zWiki!=0 || p->zWikiTitle!=0 ){
908
- if( p->zAttachName ) SYNTAX("A-card in wiki");
909
- if( p->zBaseline ) SYNTAX("B-card in wiki");
910
- if( p->rDate<=0.0 ) SYNTAX("missing date on wiki");
911
- if( p->nFile>0 ) SYNTAX("F-card in wiki");
912
- if( p->nField>0 ) SYNTAX("J-card in wiki");
913
- if( p->zTicketUuid ) SYNTAX("K-card in wiki");
914
- if( p->zWikiTitle==0 ) SYNTAX("missing L-card on wiki");
915
- if( p->zRepoCksum ) SYNTAX("R-card in wiki");
916
- if( p->nTag>0 ) SYNTAX("T-card in wiki");
917
- if( p->zWiki==0 ) SYNTAX("missing W-card on wiki");
918
- if( !seenZ ) SYNTAX("missing Z-card on wiki");
919
- p->type = CFTYPE_WIKI;
920
- }else if( hasSelfRefTag || p->nFile>0 || p->zRepoCksum!=0 || p->zBaseline
921
- || p->nParent>0 ){
922
- if( p->zAttachName ) SYNTAX("A-card in manifest");
923
- if( p->rDate<=0.0 ) SYNTAX("missing date on manifest");
924
- if( p->nField>0 ) SYNTAX("J-card in manifest");
925
- if( p->zTicketUuid ) SYNTAX("K-card in manifest");
926
- p->type = CFTYPE_MANIFEST;
927
- }else if( p->nField>0 || p->zTicketUuid!=0 ){
928
- if( p->zAttachName ) SYNTAX("A-card in ticket");
929
- if( p->rDate<=0.0 ) SYNTAX("missing date on ticket");
930
- if( p->nField==0 ) SYNTAX("missing J-card on ticket");
931
- if( p->zTicketUuid==0 ) SYNTAX("missing K-card on ticket");
932
- if( p->zMimetype) SYNTAX("N-card in ticket");
933
- if( p->nTag>0 ) SYNTAX("T-card in ticket");
934
- if( p->zUser==0 ) SYNTAX("missing U-card on ticket");
935
- if( !seenZ ) SYNTAX("missing Z-card on ticket");
936
- p->type = CFTYPE_TICKET;
937
- }else if( p->zAttachName ){
938
- if( p->rDate<=0.0 ) SYNTAX("missing date on attachment");
939
- if( p->nTag>0 ) SYNTAX("T-card in attachment");
940
- if( !seenZ ) SYNTAX("missing Z-card on attachment");
941
- p->type = CFTYPE_ATTACHMENT;
942
- }else{
943
- if( p->rDate<=0.0 ) SYNTAX("missing date on control");
944
- if( p->zMimetype ) SYNTAX("N-card in control");
945
- if( !seenZ ) SYNTAX("missing Z-card on control");
946
- p->type = CFTYPE_CONTROL;
947
- }
995
+ /* If the artifact type has not yet been determined, then compute
996
+ ** it now. */
997
+ if( p->type==0 ){
998
+ p->type = p->zComment!=0 ? CFTYPE_MANIFEST : CFTYPE_CONTROL;
999
+ }
1000
+
1001
+ /* Verify that no disallowed cards are present for this artifact type */
1002
+ m = manifest_card_mask(manifestCardTypes[p->type-1].zAllowed);
1003
+ if( seenCard & ~m ){
1004
+ sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card in %s",
1005
+ maskToType(seenCard & ~m),
1006
+ azNameOfMType[p->type-1]);
1007
+ zErr = zErrBuf;
1008
+ goto manifest_syntax_error;
1009
+ }
1010
+
1011
+ /* Verify that all required cards are present for this artifact type */
1012
+ m = manifest_card_mask(manifestCardTypes[p->type-1].zRequired);
1013
+ if( ~seenCard & m ){
1014
+ sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card missing in %s",
1015
+ maskToType(~seenCard & m),
1016
+ azNameOfMType[p->type-1]);
1017
+ zErr = zErrBuf;
1018
+ goto manifest_syntax_error;
1019
+ }
1020
+
1021
+ /* Additional checks based on artifact type */
1022
+ switch( p->type ){
1023
+ case CFTYPE_FORUM: {
1024
+ if( p->zThreadTitle && p->zInReplyTo ){
1025
+ SYNTAX("cannot have I-card and H-card in a forum post");
1026
+ }
1027
+ if( p->nParent>1 ) SYNTAX("too many arguments to P-card");
1028
+ break;
1029
+ }
1030
+ }
1031
+
9481032
md5sum_init();
9491033
if( !isRepeat ) g.parseCnt[p->type]++;
9501034
return p;
9511035
9521036
manifest_syntax_error:
9531037
{
9541038
char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
9551039
if( zUuid ){
956
- blob_appendf(pErr, "manifest [%s] ", zUuid);
1040
+ blob_appendf(pErr, "artifact [%s] ", zUuid);
9571041
fossil_free(zUuid);
9581042
}
9591043
}
9601044
if( zErr ){
9611045
blob_appendf(pErr, "line %d: %s", lineNo, zErr);
9621046
--- src/manifest.c
+++ src/manifest.c
@@ -34,10 +34,11 @@
34 #define CFTYPE_CONTROL 3
35 #define CFTYPE_WIKI 4
36 #define CFTYPE_TICKET 5
37 #define CFTYPE_ATTACHMENT 6
38 #define CFTYPE_EVENT 7
 
39
40 /*
41 ** File permissions used by Fossil internally.
42 */
43 #define PERM_REG 0 /* regular file */
@@ -76,16 +77,19 @@
76 char *zUser; /* Name of the user from the U card. */
77 char *zRepoCksum; /* MD5 checksum of the baseline content. R card. */
78 char *zWiki; /* Text of the wiki page. W card. */
79 char *zWikiTitle; /* Name of the wiki page. L card. */
80 char *zMimetype; /* Mime type of wiki or comment text. N card. */
 
81 double rEventDate; /* Date of an event. E card. */
82 char *zEventId; /* Artifact hash for an event. E card. */
83 char *zTicketUuid; /* UUID for a ticket. K card. */
84 char *zAttachName; /* Filename of an attachment. A card. */
85 char *zAttachSrc; /* Artifact hash for document being attached. A card. */
86 char *zAttachTarget; /* Ticket or wiki that attachment applies to. A card */
 
 
87 int nFile; /* Number of F cards */
88 int nFileAlloc; /* Slots allocated in aFile[] */
89 int iFile; /* Index of current file in iterator */
90 ManifestFile *aFile; /* One entry for each F-card */
91 int nParent; /* Number of parents. */
@@ -112,10 +116,42 @@
112 char *zName; /* Key or field name */
113 char *zValue; /* Value of the field */
114 } *aField; /* One for each J card */
115 };
116 #endif
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
118 /*
119 ** A cache of parsed manifests. This reduces the number of
120 ** calls to manifest_parse() when doing a rebuild.
121 */
@@ -147,10 +183,35 @@
147 if( p->pBaseline ) manifest_destroy(p->pBaseline);
148 memset(p, 0, sizeof(*p));
149 fossil_free(p);
150 }
151 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
153 /*
154 ** Add an element to the manifest cache using LRU replacement.
155 */
156 void manifest_cache_insert(Manifest *p){
@@ -352,11 +413,10 @@
352 ** The card type determines the other parameters to the card.
353 ** Cards must occur in lexicographical order.
354 */
355 Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){
356 Manifest *p;
357 int seenZ = 0;
358 int i, lineNo=0;
359 ManifestText x;
360 char cPrevType = 0;
361 char cType;
362 char *z;
@@ -364,10 +424,13 @@
364 char *zUuid;
365 int sz = 0;
366 int isRepeat, hasSelfRefTag = 0;
367 static Bag seen;
368 const char *zErr = 0;
 
 
 
369
370 if( rid==0 ){
371 isRepeat = 1;
372 }else if( bag_find(&seen, rid) ){
373 isRepeat = 1;
@@ -422,10 +485,12 @@
422 x.z = z;
423 x.zEnd = &z[n];
424 x.atEol = 1;
425 while( (cType = next_card(&x))!=0 && cType>=cPrevType ){
426 lineNo++;
 
 
427 switch( cType ){
428 /*
429 ** A <filename> <target> ?<source>?
430 **
431 ** Identifies an attachment to either a wiki page or a ticket.
@@ -454,10 +519,11 @@
454 SYNTAX("invalid source on A-card");
455 }
456 p->zAttachName = (char*)file_tail(zName);
457 p->zAttachSrc = zSrc;
458 p->zAttachTarget = zTarget;
 
459 break;
460 }
461
462 /*
463 ** B <uuid>
@@ -469,10 +535,11 @@
469 p->zBaseline = next_token(&x, &sz);
470 if( p->zBaseline==0 ) SYNTAX("missing hash on B-card");
471 if( !hname_validate(p->zBaseline,sz) ){
472 SYNTAX("invalid hash on B-card");
473 }
 
474 break;
475 }
476
477
478 /*
@@ -520,10 +587,11 @@
520 if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card");
521 p->zEventId = next_token(&x, &sz);
522 if( !hname_validate(p->zEventId, sz) ){
523 SYNTAX("malformed hash on E-card");
524 }
 
525 break;
526 }
527
528 /*
529 ** F <filename> ?<uuid>? ?<permissions>? ?<old-name>?
@@ -565,10 +633,59 @@
565 p->aFile[i].zPerm = zPerm;
566 p->aFile[i].zPrior = zPriorName;
567 if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){
568 SYNTAX("incorrect F-card sort order");
569 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570 break;
571 }
572
573 /*
574 ** J <name> ?<value>?
@@ -594,10 +711,11 @@
594 p->aField[i].zName = zName;
595 p->aField[i].zValue = zValue;
596 if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){
597 SYNTAX("incorrect J-card sort order");
598 }
 
599 break;
600 }
601
602
603 /*
@@ -611,10 +729,11 @@
611 p->zTicketUuid = next_token(&x, &sz);
612 if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size");
613 if( !validate16(p->zTicketUuid, sz) ){
614 SYNTAX("invalid K-card UUID");
615 }
 
616 break;
617 }
618
619 /*
620 ** L <wikititle>
@@ -628,10 +747,11 @@
628 if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card");
629 defossilize(p->zWikiTitle);
630 if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){
631 SYNTAX("L-card has malformed wiki name");
632 }
 
633 break;
634 }
635
636 /*
637 ** M <hash>
@@ -653,10 +773,11 @@
653 i = p->nCChild++;
654 p->azCChild[i] = zUuid;
655 if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){
656 SYNTAX("M-card in the wrong order");
657 }
 
658 break;
659 }
660
661 /*
662 ** N <uuid>
@@ -717,10 +838,11 @@
717 p->aCherrypick[n].zCPTarget = zUuid;
718 p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz);
719 if( zUuid && !hname_validate(zUuid,sz) ){
720 SYNTAX("invalid second hash on Q-card");
721 }
 
722 break;
723 }
724
725 /*
726 ** R <md5sum>
@@ -731,10 +853,11 @@
731 case 'R': {
732 if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card");
733 p->zRepoCksum = next_token(&x, &sz);
734 if( sz!=32 ) SYNTAX("wrong size cksum on R-card");
735 if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum");
 
736 break;
737 }
738
739 /*
740 ** T (+|*|-)<tagname> <uuid> ?<value>?
@@ -759,11 +882,11 @@
759 if( zUuid==0 ) SYNTAX("missing artifact hash on T-card");
760 zValue = next_token(&x, 0);
761 if( zValue ) defossilize(zValue);
762 if( hname_validate(zUuid, sz) ){
763 /* A valid artifact hash */
764 if( p->zEventId ) SYNTAX("non-self-referential T-card in event");
765 }else if( sz==1 && zUuid[0]=='*' ){
766 zUuid = 0;
767 hasSelfRefTag = 1;
768 if( p->zEventId && zName[0]!='+' ){
769 SYNTAX("propagating T-card in event");
@@ -858,104 +981,65 @@
858 */
859 case 'Z': {
860 zUuid = next_token(&x, &sz);
861 if( sz!=32 ) SYNTAX("wrong size for Z-card cksum");
862 if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum");
863 seenZ = 1;
864 break;
865 }
866 default: {
867 SYNTAX("unrecognized card");
868 }
869 }
870 }
871 if( x.z<x.zEnd ) SYNTAX("extra characters at end of card");
872
873 if( p->nCChild>0 ){
874 if( p->zAttachName
875 || p->zBaseline
876 || p->zComment
877 || p->rDate>0.0
878 || p->zEventId
879 || p->nFile>0
880 || p->nField>0
881 || p->zTicketUuid
882 || p->zWikiTitle
883 || p->zMimetype
884 || p->nParent>0
885 || p->nCherrypick>0
886 || p->zRepoCksum
887 || p->nTag>0
888 || p->zUser
889 || p->zWiki
890 ){
891 SYNTAX("cluster contains a card other than M- or Z-");
892 }
893 if( !seenZ ) SYNTAX("missing Z-card on cluster");
894 p->type = CFTYPE_CLUSTER;
895 }else if( p->zEventId ){
896 if( p->zAttachName ) SYNTAX("A-card in event");
897 if( p->zBaseline ) SYNTAX("B-card in event");
898 if( p->rDate<=0.0 ) SYNTAX("missing date on event");
899 if( p->nFile>0 ) SYNTAX("F-card in event");
900 if( p->nField>0 ) SYNTAX("J-card in event");
901 if( p->zTicketUuid ) SYNTAX("K-card in event");
902 if( p->zWikiTitle!=0 ) SYNTAX("L-card in event");
903 if( p->zRepoCksum ) SYNTAX("R-card in event");
904 if( p->zWiki==0 ) SYNTAX("missing W-card on event");
905 if( !seenZ ) SYNTAX("missing Z-card on event");
906 p->type = CFTYPE_EVENT;
907 }else if( p->zWiki!=0 || p->zWikiTitle!=0 ){
908 if( p->zAttachName ) SYNTAX("A-card in wiki");
909 if( p->zBaseline ) SYNTAX("B-card in wiki");
910 if( p->rDate<=0.0 ) SYNTAX("missing date on wiki");
911 if( p->nFile>0 ) SYNTAX("F-card in wiki");
912 if( p->nField>0 ) SYNTAX("J-card in wiki");
913 if( p->zTicketUuid ) SYNTAX("K-card in wiki");
914 if( p->zWikiTitle==0 ) SYNTAX("missing L-card on wiki");
915 if( p->zRepoCksum ) SYNTAX("R-card in wiki");
916 if( p->nTag>0 ) SYNTAX("T-card in wiki");
917 if( p->zWiki==0 ) SYNTAX("missing W-card on wiki");
918 if( !seenZ ) SYNTAX("missing Z-card on wiki");
919 p->type = CFTYPE_WIKI;
920 }else if( hasSelfRefTag || p->nFile>0 || p->zRepoCksum!=0 || p->zBaseline
921 || p->nParent>0 ){
922 if( p->zAttachName ) SYNTAX("A-card in manifest");
923 if( p->rDate<=0.0 ) SYNTAX("missing date on manifest");
924 if( p->nField>0 ) SYNTAX("J-card in manifest");
925 if( p->zTicketUuid ) SYNTAX("K-card in manifest");
926 p->type = CFTYPE_MANIFEST;
927 }else if( p->nField>0 || p->zTicketUuid!=0 ){
928 if( p->zAttachName ) SYNTAX("A-card in ticket");
929 if( p->rDate<=0.0 ) SYNTAX("missing date on ticket");
930 if( p->nField==0 ) SYNTAX("missing J-card on ticket");
931 if( p->zTicketUuid==0 ) SYNTAX("missing K-card on ticket");
932 if( p->zMimetype) SYNTAX("N-card in ticket");
933 if( p->nTag>0 ) SYNTAX("T-card in ticket");
934 if( p->zUser==0 ) SYNTAX("missing U-card on ticket");
935 if( !seenZ ) SYNTAX("missing Z-card on ticket");
936 p->type = CFTYPE_TICKET;
937 }else if( p->zAttachName ){
938 if( p->rDate<=0.0 ) SYNTAX("missing date on attachment");
939 if( p->nTag>0 ) SYNTAX("T-card in attachment");
940 if( !seenZ ) SYNTAX("missing Z-card on attachment");
941 p->type = CFTYPE_ATTACHMENT;
942 }else{
943 if( p->rDate<=0.0 ) SYNTAX("missing date on control");
944 if( p->zMimetype ) SYNTAX("N-card in control");
945 if( !seenZ ) SYNTAX("missing Z-card on control");
946 p->type = CFTYPE_CONTROL;
947 }
948 md5sum_init();
949 if( !isRepeat ) g.parseCnt[p->type]++;
950 return p;
951
952 manifest_syntax_error:
953 {
954 char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
955 if( zUuid ){
956 blob_appendf(pErr, "manifest [%s] ", zUuid);
957 fossil_free(zUuid);
958 }
959 }
960 if( zErr ){
961 blob_appendf(pErr, "line %d: %s", lineNo, zErr);
962
--- src/manifest.c
+++ src/manifest.c
@@ -34,10 +34,11 @@
34 #define CFTYPE_CONTROL 3
35 #define CFTYPE_WIKI 4
36 #define CFTYPE_TICKET 5
37 #define CFTYPE_ATTACHMENT 6
38 #define CFTYPE_EVENT 7
39 #define CFTYPE_FORUM 8
40
41 /*
42 ** File permissions used by Fossil internally.
43 */
44 #define PERM_REG 0 /* regular file */
@@ -76,16 +77,19 @@
77 char *zUser; /* Name of the user from the U card. */
78 char *zRepoCksum; /* MD5 checksum of the baseline content. R card. */
79 char *zWiki; /* Text of the wiki page. W card. */
80 char *zWikiTitle; /* Name of the wiki page. L card. */
81 char *zMimetype; /* Mime type of wiki or comment text. N card. */
82 char *zThreadTitle; /* The forum thread title. H card */
83 double rEventDate; /* Date of an event. E card. */
84 char *zEventId; /* Artifact hash for an event. E card. */
85 char *zTicketUuid; /* UUID for a ticket. K card. */
86 char *zAttachName; /* Filename of an attachment. A card. */
87 char *zAttachSrc; /* Artifact hash for document being attached. A card. */
88 char *zAttachTarget; /* Ticket or wiki that attachment applies to. A card */
89 char *zThreadRoot; /* Thread root artifact. G card */
90 char *zInReplyTo; /* Forum in-reply-to artifact. I card */
91 int nFile; /* Number of F cards */
92 int nFileAlloc; /* Slots allocated in aFile[] */
93 int iFile; /* Index of current file in iterator */
94 ManifestFile *aFile; /* One entry for each F-card */
95 int nParent; /* Number of parents. */
@@ -112,10 +116,42 @@
116 char *zName; /* Key or field name */
117 char *zValue; /* Value of the field */
118 } *aField; /* One for each J card */
119 };
120 #endif
121
122 /*
123 ** Allowed and required card types in each style of artifact
124 */
125 static struct {
126 const char *zAllowed; /* Allowed cards. Human-readable */
127 const char *zRequired; /* Required cards. Human-readable */
128 } manifestCardTypes[] = {
129 /* Allowed Required */
130 /* CFTYPE_MANIFEST 1 */ { "BCDFNPQRTUZ", "CDUZ" },
131 /* CFTYPE_CLUSTER 2 */ { "MZ", "MZ" },
132 /* CFTYPE_CONTROL 3 */ { "DTUZ", "DTUZ" },
133 /* CFTYPE_WIKI 4 */ { "DLNPUWZ", "DLUWZ" },
134 /* CFTYPE_TICKET 5 */ { "DJKUZ", "DJKUZ" },
135 /* CFTYPE_ATTACHMENT 6 */ { "ACDNUZ", "ADZ" },
136 /* CFTYPE_EVENT 7 */ { "CDENPTUWZ", "DEWZ" },
137 /* CFTYPE_FORUM 8 */ { "DGHINPUWZ", "DUWZ" },
138 };
139
140 /*
141 ** Names of manifest types
142 */
143 static const char *azNameOfMType[] = {
144 "manifest",
145 "cluster",
146 "tag",
147 "wiki",
148 "ticket",
149 "attachment",
150 "technote",
151 "forum post"
152 };
153
154 /*
155 ** A cache of parsed manifests. This reduces the number of
156 ** calls to manifest_parse() when doing a rebuild.
157 */
@@ -147,10 +183,35 @@
183 if( p->pBaseline ) manifest_destroy(p->pBaseline);
184 memset(p, 0, sizeof(*p));
185 fossil_free(p);
186 }
187 }
188
189 /*
190 ** Given a string of upper-case letters, compute a mask of the letters
191 ** present. For example, "ABC" computes 0x0007. "DE" gives 0x0018".
192 */
193 static unsigned int manifest_card_mask(const char *z){
194 unsigned int m = 0;
195 char c;
196 while( (c = *(z++))>='A' && c<='Z' ){
197 m |= 1 << (c - 'A');
198 }
199 return m;
200 }
201
202 /*
203 ** Given an integer mask representing letters A-Z, return the
204 ** letter which is the first bit set in the mask. Example:
205 ** 0x03520 gives 'F' since the F-bit is the lowest.
206 */
207 static char maskToType(unsigned int x){
208 char c = 'A';
209 if( x==0 ) return '?';
210 while( (x&1)==0 ){ x >>= 1; c++; }
211 return c;
212 }
213
214 /*
215 ** Add an element to the manifest cache using LRU replacement.
216 */
217 void manifest_cache_insert(Manifest *p){
@@ -352,11 +413,10 @@
413 ** The card type determines the other parameters to the card.
414 ** Cards must occur in lexicographical order.
415 */
416 Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){
417 Manifest *p;
 
418 int i, lineNo=0;
419 ManifestText x;
420 char cPrevType = 0;
421 char cType;
422 char *z;
@@ -364,10 +424,13 @@
424 char *zUuid;
425 int sz = 0;
426 int isRepeat, hasSelfRefTag = 0;
427 static Bag seen;
428 const char *zErr = 0;
429 unsigned int m;
430 unsigned int seenCard = 0; /* Which card types have been seen */
431 char zErrBuf[100]; /* Write error messages here */
432
433 if( rid==0 ){
434 isRepeat = 1;
435 }else if( bag_find(&seen, rid) ){
436 isRepeat = 1;
@@ -422,10 +485,12 @@
485 x.z = z;
486 x.zEnd = &z[n];
487 x.atEol = 1;
488 while( (cType = next_card(&x))!=0 && cType>=cPrevType ){
489 lineNo++;
490 if( cType<'A' || cType>'Z' ) SYNTAX("bad card type");
491 seenCard |= 1 << (cType-'A');
492 switch( cType ){
493 /*
494 ** A <filename> <target> ?<source>?
495 **
496 ** Identifies an attachment to either a wiki page or a ticket.
@@ -454,10 +519,11 @@
519 SYNTAX("invalid source on A-card");
520 }
521 p->zAttachName = (char*)file_tail(zName);
522 p->zAttachSrc = zSrc;
523 p->zAttachTarget = zTarget;
524 p->type = CFTYPE_ATTACHMENT;
525 break;
526 }
527
528 /*
529 ** B <uuid>
@@ -469,10 +535,11 @@
535 p->zBaseline = next_token(&x, &sz);
536 if( p->zBaseline==0 ) SYNTAX("missing hash on B-card");
537 if( !hname_validate(p->zBaseline,sz) ){
538 SYNTAX("invalid hash on B-card");
539 }
540 p->type = CFTYPE_MANIFEST;
541 break;
542 }
543
544
545 /*
@@ -520,10 +587,11 @@
587 if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card");
588 p->zEventId = next_token(&x, &sz);
589 if( !hname_validate(p->zEventId, sz) ){
590 SYNTAX("malformed hash on E-card");
591 }
592 p->type = CFTYPE_EVENT;
593 break;
594 }
595
596 /*
597 ** F <filename> ?<uuid>? ?<permissions>? ?<old-name>?
@@ -565,10 +633,59 @@
633 p->aFile[i].zPerm = zPerm;
634 p->aFile[i].zPrior = zPriorName;
635 if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){
636 SYNTAX("incorrect F-card sort order");
637 }
638 p->type = CFTYPE_MANIFEST;
639 break;
640 }
641
642 /*
643 ** G <hash>
644 **
645 ** A G-card identifies the initial root forum post for the thread
646 ** of which this post is a part. Forum posts only.
647 */
648 case 'G': {
649 if( p->zThreadRoot!=0 ) SYNTAX("more than one G-card");
650 p->zThreadRoot = next_token(&x, &sz);
651 if( p->zThreadRoot==0 ) SYNTAX("missing hash on G-card");
652 if( !hname_validate(p->zThreadRoot,sz) ){
653 SYNTAX("Invalid hash on G-card");
654 }
655 p->type = CFTYPE_FORUM;
656 break;
657 }
658
659 /*
660 ** H <threadtitle>
661 **
662 ** The title for a forum thread.
663 */
664 case 'H': {
665 if( p->zThreadTitle!=0 ) SYNTAX("more than one H-card");
666 p->zThreadTitle = next_token(&x,0);
667 if( p->zThreadTitle==0 ) SYNTAX("missing title on H-card");
668 defossilize(p->zThreadTitle);
669 p->type = CFTYPE_FORUM;
670 break;
671 }
672
673 /*
674 ** I <hash>
675 **
676 ** A I-card identifies another forum post that the current forum post
677 ** is in reply to.
678 */
679 case 'I': {
680 if( p->zInReplyTo!=0 ) SYNTAX("more than one I-card");
681 p->zInReplyTo = next_token(&x, &sz);
682 if( p->zInReplyTo==0 ) SYNTAX("missing hash on I-card");
683 if( !hname_validate(p->zInReplyTo,sz) ){
684 SYNTAX("Invalid hash on I-card");
685 }
686 p->type = CFTYPE_FORUM;
687 break;
688 }
689
690 /*
691 ** J <name> ?<value>?
@@ -594,10 +711,11 @@
711 p->aField[i].zName = zName;
712 p->aField[i].zValue = zValue;
713 if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){
714 SYNTAX("incorrect J-card sort order");
715 }
716 p->type = CFTYPE_TICKET;
717 break;
718 }
719
720
721 /*
@@ -611,10 +729,11 @@
729 p->zTicketUuid = next_token(&x, &sz);
730 if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size");
731 if( !validate16(p->zTicketUuid, sz) ){
732 SYNTAX("invalid K-card UUID");
733 }
734 p->type = CFTYPE_TICKET;
735 break;
736 }
737
738 /*
739 ** L <wikititle>
@@ -628,10 +747,11 @@
747 if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card");
748 defossilize(p->zWikiTitle);
749 if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){
750 SYNTAX("L-card has malformed wiki name");
751 }
752 p->type = CFTYPE_WIKI;
753 break;
754 }
755
756 /*
757 ** M <hash>
@@ -653,10 +773,11 @@
773 i = p->nCChild++;
774 p->azCChild[i] = zUuid;
775 if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){
776 SYNTAX("M-card in the wrong order");
777 }
778 p->type = CFTYPE_CLUSTER;
779 break;
780 }
781
782 /*
783 ** N <uuid>
@@ -717,10 +838,11 @@
838 p->aCherrypick[n].zCPTarget = zUuid;
839 p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz);
840 if( zUuid && !hname_validate(zUuid,sz) ){
841 SYNTAX("invalid second hash on Q-card");
842 }
843 p->type = CFTYPE_MANIFEST;
844 break;
845 }
846
847 /*
848 ** R <md5sum>
@@ -731,10 +853,11 @@
853 case 'R': {
854 if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card");
855 p->zRepoCksum = next_token(&x, &sz);
856 if( sz!=32 ) SYNTAX("wrong size cksum on R-card");
857 if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum");
858 p->type = CFTYPE_MANIFEST;
859 break;
860 }
861
862 /*
863 ** T (+|*|-)<tagname> <uuid> ?<value>?
@@ -759,11 +882,11 @@
882 if( zUuid==0 ) SYNTAX("missing artifact hash on T-card");
883 zValue = next_token(&x, 0);
884 if( zValue ) defossilize(zValue);
885 if( hname_validate(zUuid, sz) ){
886 /* A valid artifact hash */
887 if( p->zEventId ) SYNTAX("non-self-referential T-card in technote");
888 }else if( sz==1 && zUuid[0]=='*' ){
889 zUuid = 0;
890 hasSelfRefTag = 1;
891 if( p->zEventId && zName[0]!='+' ){
892 SYNTAX("propagating T-card in event");
@@ -858,104 +981,65 @@
981 */
982 case 'Z': {
983 zUuid = next_token(&x, &sz);
984 if( sz!=32 ) SYNTAX("wrong size for Z-card cksum");
985 if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum");
 
986 break;
987 }
988 default: {
989 SYNTAX("unrecognized card");
990 }
991 }
992 }
993 if( x.z<x.zEnd ) SYNTAX("extra characters at end of card");
994
995 /* If the artifact type has not yet been determined, then compute
996 ** it now. */
997 if( p->type==0 ){
998 p->type = p->zComment!=0 ? CFTYPE_MANIFEST : CFTYPE_CONTROL;
999 }
1000
1001 /* Verify that no disallowed cards are present for this artifact type */
1002 m = manifest_card_mask(manifestCardTypes[p->type-1].zAllowed);
1003 if( seenCard & ~m ){
1004 sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card in %s",
1005 maskToType(seenCard & ~m),
1006 azNameOfMType[p->type-1]);
1007 zErr = zErrBuf;
1008 goto manifest_syntax_error;
1009 }
1010
1011 /* Verify that all required cards are present for this artifact type */
1012 m = manifest_card_mask(manifestCardTypes[p->type-1].zRequired);
1013 if( ~seenCard & m ){
1014 sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card missing in %s",
1015 maskToType(~seenCard & m),
1016 azNameOfMType[p->type-1]);
1017 zErr = zErrBuf;
1018 goto manifest_syntax_error;
1019 }
1020
1021 /* Additional checks based on artifact type */
1022 switch( p->type ){
1023 case CFTYPE_FORUM: {
1024 if( p->zThreadTitle && p->zInReplyTo ){
1025 SYNTAX("cannot have I-card and H-card in a forum post");
1026 }
1027 if( p->nParent>1 ) SYNTAX("too many arguments to P-card");
1028 break;
1029 }
1030 }
1031
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1032 md5sum_init();
1033 if( !isRepeat ) g.parseCnt[p->type]++;
1034 return p;
1035
1036 manifest_syntax_error:
1037 {
1038 char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
1039 if( zUuid ){
1040 blob_appendf(pErr, "artifact [%s] ", zUuid);
1041 fossil_free(zUuid);
1042 }
1043 }
1044 if( zErr ){
1045 blob_appendf(pErr, "line %d: %s", lineNo, zErr);
1046
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -692,10 +692,21 @@
692692
<td>&nbsp;</td>
693693
<td>&nbsp;</td>
694694
<td>&nbsp;</td>
695695
<td>&nbsp;</td>
696696
</tr>
697
+<tr>
698
+<td><b>G</b> <i>thread-root</i></td>
699
+<td>&nbsp;</td>
700
+<td>&nbsp;</td>
701
+<td>&nbsp;</td>
702
+<td>&nbsp;</td>
703
+<td>&nbsp;</td>
704
+<td>&nbsp;</td>
705
+<td>&nbsp;</td>
706
+<td align=center><b>0-1</b></td>
707
+</tr>
697708
<tr>
698709
<td><b>H</b> <i>thread-title</i></td>
699710
<td>&nbsp;</td>
700711
<td>&nbsp;</td>
701712
<td>&nbsp;</td>
702713
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -692,10 +692,21 @@
692 <td>&nbsp;</td>
693 <td>&nbsp;</td>
694 <td>&nbsp;</td>
695 <td>&nbsp;</td>
696 </tr>
 
 
 
 
 
 
 
 
 
 
 
697 <tr>
698 <td><b>H</b> <i>thread-title</i></td>
699 <td>&nbsp;</td>
700 <td>&nbsp;</td>
701 <td>&nbsp;</td>
702
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -692,10 +692,21 @@
692 <td>&nbsp;</td>
693 <td>&nbsp;</td>
694 <td>&nbsp;</td>
695 <td>&nbsp;</td>
696 </tr>
697 <tr>
698 <td><b>G</b> <i>thread-root</i></td>
699 <td>&nbsp;</td>
700 <td>&nbsp;</td>
701 <td>&nbsp;</td>
702 <td>&nbsp;</td>
703 <td>&nbsp;</td>
704 <td>&nbsp;</td>
705 <td>&nbsp;</td>
706 <td align=center><b>0-1</b></td>
707 </tr>
708 <tr>
709 <td><b>H</b> <i>thread-title</i></td>
710 <td>&nbsp;</td>
711 <td>&nbsp;</td>
712 <td>&nbsp;</td>
713

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button