github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/logbook/oplog/log.go (about) 1 // Package oplog is an operation-based replicated data type of append-only logs 2 // oplog has three main structures: logbook, log, and op 3 // A log is a sequence of operations attributed to a single author, designated 4 // by a private key. 5 // an operation is a record of some action an author took. Applications iterate 6 // the sequence of operations to produce the current state. 7 // Logs can be arranged into hierarchies to form logical groupings. 8 // A book contains an author's logs, both logs they've written as well as logs 9 // replicated from other authors. Books are encrypted at rest using the author 10 // private key. 11 package oplog 12 13 import ( 14 "context" 15 "crypto/aes" 16 "crypto/cipher" 17 "crypto/md5" 18 "crypto/rand" 19 "encoding/base32" 20 "encoding/hex" 21 "errors" 22 "fmt" 23 "io" 24 25 flatbuffers "github.com/google/flatbuffers/go" 26 crypto "github.com/libp2p/go-libp2p-core/crypto" 27 "github.com/qri-io/qri/logbook/oplog/logfb" 28 "golang.org/x/crypto/blake2b" 29 ) 30 31 var ( 32 // ErrNotFound is a sentinel error for data not found in a logbook 33 ErrNotFound = fmt.Errorf("log: not found") 34 ) 35 36 // Logstore persists a set of operations organized into hierarchical append-only 37 // logs 38 type Logstore interface { 39 // MergeLog adds a Log to the store, controlling for conflicts 40 // * logs that are already known to the store are merged with a 41 // longest-log-wins strategy, adding all descendants 42 // * new top level logs are appended to the store, including all descendants 43 // * attempting to add a log with a parent not already in the store MUST fail 44 // 45 // TODO (b5) - currently a Log pointer doesn't provide a clear method for 46 // getting the ID of it's parent, which negates the potential for attempting 47 // to merge child log, so we don't need to control for the third point quite 48 // yet 49 MergeLog(ctx context.Context, l *Log) error 50 51 // Remove a log from the store, all descendant logs must be removed as well 52 RemoveLog(ctx context.Context, names ...string) error 53 54 // Logs lists top level logs in the store, that is, the set logs that have no 55 // parent. passing -1 as a limit returns all top level logs after the offset 56 // 57 // The order of logs returned is up to the store, but the stored order must 58 // be deterministic 59 Logs(ctx context.Context, offset, limit int) (topLevel []*Log, err error) 60 61 // get a log according to a hierarchy of log.Name() references 62 // for example, fetching HeadRef(ctx, "foo", "bar", "baz") is a request 63 // for the log at the hierarchy foo/bar/baz: 64 // foo 65 // bar 66 // baz 67 // 68 // HeadRef must return ErrNotFound if any name in the heirarchy is missing 69 // from the store 70 // Head references are mutated by adding operations to a log that modifies 71 // the name of the initialization model, which means names are not a 72 // persistent identifier 73 // 74 // HeadRef MAY return children of a log. If the returned log.Log value is 75 // populated, it MUST contain all children of the log. 76 // use Logstore.ChildrenĀ or Logstore.Descendants to populate missing children 77 HeadRef(ctx context.Context, names ...string) (*Log, error) 78 79 // get a log according to it's ID string 80 // Log must return ErrNotFound if the ID does not exist 81 // 82 // Log MAY return children of a log. If the returned log.Log value is 83 // populated, it MUST contain all children of the log. 84 // use Logstore.ChildrenĀ or Logstore.Descendants to populate missing children 85 Get(ctx context.Context, id string) (*Log, error) 86 87 // GetAuthorID fetches the first log that matches the given model and 88 // authorID. Note that AutorID is the initial operation AuthorID field, 89 // not the init identifier 90 GetAuthorID(ctx context.Context, model uint32, authorID string) (*Log, error) 91 92 // get the immediate descendants of a log, using the given log as an outparam. 93 // Children must only mutate Logs field of the passed-in log pointer 94 // added Children MAY include decendant logs 95 Children(ctx context.Context, l *Log) error 96 // get all generations of a log, using the given log as an outparam 97 // Descendants MUST only mutate Logs field of the passed-in log pointer 98 Descendants(ctx context.Context, l *Log) error 99 100 // ReplaceAll replaces the contents of the entire log 101 ReplaceAll(ctx context.Context, l *Log) error 102 } 103 104 // SparseAncestorsAllDescendantsLogstore is a an extension interface to 105 // Logstore with an optimized method for getting a log with sparse parents and 106 // all descendants 107 type SparseAncestorsAllDescendantsLogstore interface { 108 Logstore 109 // GetSparseAncestorsAllDescendants is a fast-path method for getting a 110 // log that includes sparse parents & complete descendants. "sparse parents" 111 // have the only children given in parent specified 112 // AllDescendants include 113 // the returned log will match the ID of the request, with parents 114 GetSparseAncestorsAllDescendants(ctx context.Context, id string) (*Log, error) 115 } 116 117 // GetWithSparseAncestorsAllDescendants is a fast-path method for getting a 118 // log that includes sparse parents & complete descendants. "sparse parents" 119 // means the only children given in parent 120 // the returned log will match the ID of the request, with parents 121 func GetWithSparseAncestorsAllDescendants(ctx context.Context, store Logstore, id string) (*Log, error) { 122 if sparseLS, ok := store.(SparseAncestorsAllDescendantsLogstore); ok { 123 return sparseLS.GetSparseAncestorsAllDescendants(ctx, id) 124 } 125 126 l, err := store.Get(ctx, id) 127 if err != nil { 128 return nil, err 129 } 130 131 // TODO (b5) - this is error prone. logs may exist but not be fetched. This 132 // check is here now because not all implementations support the Descendants 133 // method properly. 134 // in the future remove this check once all implementations we know of have 135 // a working `Descendants` implementation 136 if len(l.Logs) == 0 { 137 if err = store.Descendants(ctx, l); err != nil { 138 return nil, err 139 } 140 } 141 142 cursor := l 143 for cursor.ParentID != "" { 144 parent := cursor.parent 145 if parent == nil { 146 if parent, err = store.Get(ctx, cursor.ParentID); err != nil { 147 return nil, err 148 } 149 } 150 151 // TODO (b5) - hack to carry signatures possibly stored on the child 152 // this is incorrect long term, but has the effect of producing correct 153 // signed logs. Need to switch to log-specific signatures ASAP 154 if parent.Signature == nil { 155 parent.Signature = cursor.Signature 156 } 157 158 cursor.parent = &Log{ 159 ParentID: parent.ParentID, 160 Ops: parent.Ops, 161 Logs: []*Log{cursor}, 162 Signature: parent.Signature, 163 } 164 cursor = cursor.parent 165 } 166 167 return l, nil 168 } 169 170 // AuthorLogstore describes a store owned by a single author, it adds encryption 171 // methods for safe local persistence as well as owner ID accessors 172 type AuthorLogstore interface { 173 // All AuthorLogstores are Logstores 174 Logstore 175 // marshals all logs to a slice of bytes encrypted with the given private key 176 FlatbufferCipher(pk crypto.PrivKey) ([]byte, error) 177 // decrypt flatbuffer bytes, re-hydrating the store 178 UnmarshalFlatbufferCipher(ctx context.Context, pk crypto.PrivKey, ciphertext []byte) error 179 } 180 181 // Journal is a store of logs known to a single author, representing their 182 // view of an abstract dataset graph. journals live in memory by default, and 183 // can be encrypted for storage 184 type Journal struct { 185 logs []*Log 186 } 187 188 // assert at compile time that a Journal pointer is an AuthorLogstore 189 var _ AuthorLogstore = (*Journal)(nil) 190 191 // MergeLog adds a log to the journal 192 func (j *Journal) MergeLog(ctx context.Context, incoming *Log) error { 193 if incoming.ID() == "" { 194 return fmt.Errorf("oplog: log ID cannot be empty") 195 } 196 197 // Find a log with a matching id 198 found, err := j.Get(ctx, incoming.ID()) 199 if err == nil { 200 // If found, merge it 201 found.Merge(incoming) 202 return nil 203 } else if !errors.Is(err, ErrNotFound) { 204 // Okay if log is not found by id, but any other error should be returned 205 return err 206 } 207 208 // Find a user log with a matching profileID 209 for _, lg := range j.logs { 210 if lg.FirstOpAuthorID() == incoming.FirstOpAuthorID() { 211 found = lg 212 found.Merge(incoming) 213 return nil 214 } 215 } 216 217 // Append to the end 218 j.logs = append(j.logs, incoming) 219 return nil 220 } 221 222 // RemoveLog removes a log from the journal 223 // TODO (b5) - this currently won't work when trying to remove the root log 224 func (j *Journal) RemoveLog(ctx context.Context, names ...string) error { 225 if len(names) == 0 { 226 return fmt.Errorf("name is required") 227 } 228 229 remove := names[len(names)-1] 230 parentPath := names[:len(names)-1] 231 232 if len(parentPath) == 0 { 233 for i, l := range j.logs { 234 if l.Name() == remove { 235 j.logs = append(j.logs[:i], j.logs[i+1:]...) 236 return nil 237 } 238 } 239 return ErrNotFound 240 } 241 242 parent, err := j.HeadRef(ctx, parentPath...) 243 if err != nil { 244 return err 245 } 246 247 // iterate list looking for log to remove 248 for i, l := range parent.Logs { 249 if l.Name() == remove { 250 parent.Logs = append(parent.Logs[:i], parent.Logs[i+1:]...) 251 return nil 252 } 253 } 254 255 return ErrNotFound 256 } 257 258 // Get fetches a log for a given ID 259 func (j *Journal) Get(_ context.Context, id string) (*Log, error) { 260 for _, lg := range j.logs { 261 if l, err := lg.Log(id); err == nil { 262 return l, nil 263 } 264 } 265 return nil, ErrNotFound 266 } 267 268 // GetAuthorID fetches the first log that matches the given model and authorID 269 func (j *Journal) GetAuthorID(_ context.Context, model uint32, authorID string) (*Log, error) { 270 for _, lg := range j.logs { 271 // NOTE: old logbook entries erroneously used logbook identifiers in the AuthorID 272 // space when they should have been using external author Identifiers. In the short 273 // term we're relying on the fact that the 0th operation always uses an external 274 // identifier 275 if lg.Model() == model && lg.FirstOpAuthorID() == authorID { 276 return lg, nil 277 } 278 } 279 return nil, fmt.Errorf("getting log by author ID %q %w", authorID, ErrNotFound) 280 } 281 282 // HeadRef traverses the log graph & pulls out a log based on named head 283 // references 284 // HeadRef will not return logs that have been marked as removed. To fetch 285 // removed logs either traverse the entire journal or reference a log by ID 286 func (j *Journal) HeadRef(_ context.Context, names ...string) (*Log, error) { 287 if len(names) == 0 { 288 return nil, fmt.Errorf("name is required") 289 } 290 291 for _, log := range j.logs { 292 if log.Name() == names[0] && !log.Removed() { 293 return log.HeadRef(names[1:]...) 294 } 295 } 296 return nil, ErrNotFound 297 } 298 299 // Logs returns the full map of logs keyed by model type 300 func (j *Journal) Logs(ctx context.Context, offset, limit int) (topLevel []*Log, err error) { 301 // fast-path for no pagination 302 if offset == 0 && limit == -1 { 303 return j.logs[:], nil 304 } 305 306 return nil, fmt.Errorf("log subsets not finished") 307 } 308 309 // UnmarshalFlatbufferCipher decrypts and loads a flatbuffer ciphertext 310 func (j *Journal) UnmarshalFlatbufferCipher(ctx context.Context, pk crypto.PrivKey, ciphertext []byte) error { 311 plaintext, err := j.decrypt(pk, ciphertext) 312 if err != nil { 313 return err 314 } 315 316 return j.unmarshalFlatbuffer(logfb.GetRootAsBook(plaintext, 0)) 317 } 318 319 // Children gets all descentants of a log, because logbook stores all 320 // descendants in memory, children is a proxy for descenants 321 func (j *Journal) Children(ctx context.Context, l *Log) error { 322 return j.Descendants(ctx, l) 323 } 324 325 // Descendants gets all descentants of a log & assigns the results to the given 326 // Log parameter, setting only the Logs field 327 func (j *Journal) Descendants(ctx context.Context, l *Log) error { 328 got, err := j.Get(ctx, l.ID()) 329 if err != nil { 330 return err 331 } 332 333 l.Logs = got.Logs 334 return nil 335 } 336 337 // ReplaceAll replaces the entirety of the logs 338 func (j *Journal) ReplaceAll(ctx context.Context, l *Log) error { 339 j.logs = []*Log{l} 340 return nil 341 } 342 343 // FlatbufferCipher marshals journal to a flatbuffer and encrypts the book using 344 // a given private key. This same private key must be retained elsewhere to read 345 // the flatbuffer later on 346 func (j Journal) FlatbufferCipher(pk crypto.PrivKey) ([]byte, error) { 347 return j.encrypt(pk, j.flatbufferBytes()) 348 } 349 350 func (j Journal) cipher(pk crypto.PrivKey) (cipher.AEAD, error) { 351 pkBytes, err := pk.Raw() 352 if err != nil { 353 return nil, err 354 } 355 hasher := md5.New() 356 hasher.Write(pkBytes) 357 hash := hex.EncodeToString(hasher.Sum(nil)) 358 359 block, err := aes.NewCipher([]byte(hash)) 360 if err != nil { 361 return nil, err 362 } 363 return cipher.NewGCM(block) 364 } 365 366 func (j Journal) encrypt(pk crypto.PrivKey, data []byte) ([]byte, error) { 367 gcm, err := j.cipher(pk) 368 if err != nil { 369 return nil, err 370 } 371 372 nonce := make([]byte, gcm.NonceSize()) 373 if _, err = io.ReadFull(rand.Reader, nonce); err != nil { 374 return nil, err 375 } 376 377 ciphertext := gcm.Seal(nonce, nonce, data, nil) 378 return ciphertext, nil 379 } 380 381 func (j Journal) decrypt(pk crypto.PrivKey, data []byte) ([]byte, error) { 382 gcm, err := j.cipher(pk) 383 if err != nil { 384 return nil, err 385 } 386 387 nonceSize := gcm.NonceSize() 388 nonce, ciphertext := data[:nonceSize], data[nonceSize:] 389 plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) 390 if err != nil { 391 return nil, err 392 } 393 return plaintext, nil 394 } 395 396 // flatbufferBytes formats book as a flatbuffer byte slice 397 func (j Journal) flatbufferBytes() []byte { 398 builder := flatbuffers.NewBuilder(0) 399 off := j.marshalFlatbuffer(builder) 400 builder.Finish(off) 401 return builder.FinishedBytes() 402 } 403 404 // note: currently doesn't marshal book.author, we're considering deprecating 405 // the author field 406 func (j Journal) marshalFlatbuffer(builder *flatbuffers.Builder) flatbuffers.UOffsetT { 407 408 setsl := j.logs 409 count := len(setsl) 410 offsets := make([]flatbuffers.UOffsetT, count) 411 for i, lset := range setsl { 412 offsets[i] = lset.MarshalFlatbuffer(builder) 413 } 414 logfb.BookStartLogsVector(builder, count) 415 for i := count - 1; i >= 0; i-- { 416 builder.PrependUOffsetT(offsets[i]) 417 } 418 sets := builder.EndVector(count) 419 420 logfb.BookStart(builder) 421 logfb.BookAddLogs(builder, sets) 422 return logfb.BookEnd(builder) 423 } 424 425 func (j *Journal) unmarshalFlatbuffer(b *logfb.Book) error { 426 newBook := Journal{} 427 428 count := b.LogsLength() 429 lfb := &logfb.Log{} 430 for i := 0; i < count; i++ { 431 if b.Logs(lfb, i) { 432 l := &Log{} 433 if err := l.UnmarshalFlatbuffer(lfb, nil); err != nil { 434 return err 435 } 436 newBook.logs = append(newBook.logs, l) 437 } 438 } 439 440 *j = newBook 441 return nil 442 } 443 444 // Log is a causally-ordered set of operations performed by a single author. 445 // log attribution is verified by an author's signature 446 type Log struct { 447 name string // name value cache. not persisted 448 authorID string // authorID value cache. not persisted 449 parent *Log // parent link 450 451 ParentID string // init id of parent Log 452 Signature []byte 453 Ops []Op 454 Logs []*Log 455 } 456 457 // InitLog creates a Log from an initialization operation 458 func InitLog(initop Op) *Log { 459 return &Log{ 460 Ops: []Op{initop}, 461 } 462 } 463 464 // FromFlatbufferBytes initializes a log from flatbuffer data 465 func FromFlatbufferBytes(data []byte) (*Log, error) { 466 rootfb := logfb.GetRootAsLog(data, 0) 467 lg := &Log{} 468 return lg, lg.UnmarshalFlatbuffer(rootfb, nil) 469 } 470 471 // Append adds an operation to the log 472 func (lg *Log) Append(op Op) { 473 if op.Model == lg.Model() { 474 if op.Name != "" { 475 lg.name = op.Name 476 } 477 if op.AuthorID != "" { 478 lg.authorID = op.AuthorID 479 } 480 } 481 lg.Ops = append(lg.Ops, op) 482 } 483 484 // ID returns the hash of the initialization operation 485 // if the log is empty, returns the empty string 486 func (lg Log) ID() string { 487 if len(lg.Ops) == 0 { 488 return "" 489 } 490 return lg.Ops[0].Hash() 491 } 492 493 // Head gets the latest operation in the log 494 func (lg Log) Head() Op { 495 if len(lg.Ops) == 0 { 496 return Op{} 497 } 498 return lg.Ops[len(lg.Ops)-1] 499 } 500 501 // Model gives the operation type for a log, based on the first operation 502 // written to the log. Logs can contain multiple models of operations, but the 503 // first operation written to a log determines the kind of log for 504 // catagorization purposes 505 func (lg Log) Model() uint32 { 506 return lg.Ops[0].Model 507 } 508 509 // Author returns one of two different things: either the user's ProfileID, 510 // or the has of the first Op for the UserLog, depending on if they have 511 // ever changed their username. 512 func (lg Log) Author() (identifier string) { 513 if lg.authorID == "" { 514 m := lg.Model() 515 for _, o := range lg.Ops { 516 if o.Model == m && o.AuthorID != "" { 517 lg.authorID = o.AuthorID 518 } 519 } 520 } 521 return lg.authorID 522 } 523 524 // FirstOpAuthorID returns the authorID of the first Op. For UserLog, this is ProfileID 525 func (lg Log) FirstOpAuthorID() string { 526 m := lg.Model() 527 for _, o := range lg.Ops { 528 if o.Model == m && o.AuthorID != "" { 529 return o.AuthorID 530 } 531 } 532 return "" 533 } 534 535 // Parent returns this log's parent if one exists 536 func (lg *Log) Parent() *Log { 537 return lg.parent 538 } 539 540 // Name returns the human-readable name for this log, determined by the 541 // initialization event 542 func (lg Log) Name() string { 543 if lg.name == "" { 544 m := lg.Model() 545 for _, o := range lg.Ops { 546 if o.Model == m && o.Name != "" { 547 lg.name = o.Name 548 } 549 } 550 } 551 return lg.name 552 } 553 554 // Removed returns true if the log contains a remove operation for the log model 555 func (lg Log) Removed() bool { 556 m := lg.Model() 557 for _, op := range lg.Ops { 558 if op.Model == m && op.Type == OpTypeRemove { 559 return true 560 } 561 } 562 return false 563 } 564 565 // DeepCopy produces a fresh duplicate of this log 566 func (lg *Log) DeepCopy() *Log { 567 lg.FlatbufferBytes() 568 cp := &Log{} 569 if err := cp.UnmarshalFlatbufferBytes(lg.FlatbufferBytes()); err != nil { 570 panic(err) 571 } 572 cp.ParentID = lg.ParentID 573 return cp 574 } 575 576 // Log fetches a log by ID, checking the current log and all descendants for an 577 // exact match 578 func (lg *Log) Log(id string) (*Log, error) { 579 if lg.ID() == id { 580 return lg, nil 581 } 582 if len(lg.Logs) > 0 { 583 for _, l := range lg.Logs { 584 if got, err := l.Log(id); err == nil { 585 return got, nil 586 } 587 } 588 } 589 return nil, ErrNotFound 590 } 591 592 // HeadRef returns a descendant log, traversing the log tree by name 593 // HeadRef will not return logs that have been marked as removed. To fetch 594 // removed logs either traverse the entire book or reference a log by ID 595 func (lg *Log) HeadRef(names ...string) (*Log, error) { 596 if len(names) == 0 { 597 return lg, nil 598 } 599 600 for _, log := range lg.Logs { 601 if log.Name() == names[0] && !log.Removed() { 602 return log.HeadRef(names[1:]...) 603 } 604 } 605 return nil, ErrNotFound 606 } 607 608 // AddChild appends a log as a direct descendant of this log, controlling 609 // for duplicates 610 func (lg *Log) AddChild(l *Log) { 611 l.ParentID = lg.ID() 612 l.parent = lg 613 for i, ch := range lg.Logs { 614 if ch.ID() == l.ID() { 615 if len(l.Ops) > len(ch.Ops) { 616 lg.Logs[i] = l 617 } 618 return 619 } 620 } 621 lg.Logs = append(lg.Logs, l) 622 } 623 624 // Merge combines two logs that are assumed to be a shared root, combining 625 // children from both branches, matching branches prefer longer Opsets 626 // Merging relies on comparison of initialization operations, which 627 // must be present to constitute a match 628 func (lg *Log) Merge(l *Log) { 629 // if the incoming log has more operations, use it & clear the cache 630 if len(l.Ops) > len(lg.Ops) { 631 lg.Ops = l.Ops 632 lg.name = "" 633 lg.authorID = "" 634 lg.Signature = nil 635 } 636 637 LOOP: 638 for _, x := range l.Logs { 639 for j, y := range lg.Logs { 640 // if logs match. merge 'em 641 if x.Ops[0].Equal(y.Ops[0]) { 642 lg.Logs[j].Merge(x) 643 continue LOOP 644 } 645 } 646 // no match, append! 647 lg.AddChild(x) 648 } 649 } 650 651 // Verify confirms that the signature for a log matches 652 func (lg Log) Verify(pub crypto.PubKey) error { 653 ok, err := pub.Verify(lg.SigningBytes(), lg.Signature) 654 if err != nil { 655 return err 656 } 657 if !ok { 658 return fmt.Errorf("invalid signature") 659 } 660 return nil 661 } 662 663 // Sign assigns the log signature by signing the logging checksum with a given 664 // private key 665 // TODO (b5) - this is assuming the log is authored by this private key. as soon 666 // as we add collaborators, this won't be true 667 func (lg *Log) Sign(pk crypto.PrivKey) (err error) { 668 lg.Signature, err = pk.Sign(lg.SigningBytes()) 669 if err != nil { 670 return err 671 } 672 673 return nil 674 } 675 676 // SigningBytes perpares a byte slice for signing from a log's operations 677 func (lg Log) SigningBytes() []byte { 678 hasher := md5.New() 679 for _, op := range lg.Ops { 680 hasher.Write([]byte(op.Ref)) 681 } 682 return hasher.Sum(nil) 683 } 684 685 // FlatbufferBytes marshals a log to flabuffer-formatted bytes 686 func (lg Log) FlatbufferBytes() []byte { 687 builder := flatbuffers.NewBuilder(0) 688 log := lg.MarshalFlatbuffer(builder) 689 builder.Finish(log) 690 return builder.FinishedBytes() 691 } 692 693 // MarshalFlatbuffer writes log to a flatbuffer, returning the ending byte 694 // offset 695 func (lg Log) MarshalFlatbuffer(builder *flatbuffers.Builder) flatbuffers.UOffsetT { 696 // build logs bottom up, collecting offsets 697 logcount := len(lg.Logs) 698 logoffsets := make([]flatbuffers.UOffsetT, logcount) 699 for i, o := range lg.Logs { 700 logoffsets[i] = o.MarshalFlatbuffer(builder) 701 } 702 703 logfb.LogStartLogsVector(builder, logcount) 704 for i := logcount - 1; i >= 0; i-- { 705 builder.PrependUOffsetT(logoffsets[i]) 706 } 707 logs := builder.EndVector(logcount) 708 709 name := builder.CreateString(lg.Name()) 710 id := builder.CreateString(lg.Author()) 711 signature := builder.CreateByteString(lg.Signature) 712 713 count := len(lg.Ops) 714 offsets := make([]flatbuffers.UOffsetT, count) 715 for i, o := range lg.Ops { 716 offsets[i] = o.MarshalFlatbuffer(builder) 717 } 718 719 logfb.LogStartOpsetVector(builder, count) 720 for i := count - 1; i >= 0; i-- { 721 builder.PrependUOffsetT(offsets[i]) 722 } 723 ops := builder.EndVector(count) 724 725 logfb.LogStart(builder) 726 logfb.LogAddName(builder, name) 727 logfb.LogAddIdentifier(builder, id) 728 logfb.LogAddSignature(builder, signature) 729 logfb.LogAddOpset(builder, ops) 730 logfb.LogAddLogs(builder, logs) 731 return logfb.LogEnd(builder) 732 } 733 734 // UnmarshalFlatbufferBytes is a convenince wrapper to deserialze a flatbuffer 735 // slice into a log 736 func (lg *Log) UnmarshalFlatbufferBytes(data []byte) error { 737 return lg.UnmarshalFlatbuffer(logfb.GetRootAsLog(data, 0), nil) 738 } 739 740 // UnmarshalFlatbuffer populates a logfb.Log from a Log pointer 741 func (lg *Log) UnmarshalFlatbuffer(lfb *logfb.Log, parent *Log) (err error) { 742 newLg := Log{parent: parent} 743 if parent != nil { 744 newLg.ParentID = parent.ID() 745 } 746 747 if len(lfb.Signature()) != 0 { 748 newLg.Signature = lfb.Signature() 749 } 750 751 newLg.Ops = make([]Op, lfb.OpsetLength()) 752 opfb := &logfb.Operation{} 753 for i := 0; i < lfb.OpsetLength(); i++ { 754 if lfb.Opset(opfb, i) { 755 newLg.Ops[i] = UnmarshalOpFlatbuffer(opfb) 756 } 757 } 758 759 if lfb.LogsLength() > 0 { 760 newLg.Logs = make([]*Log, lfb.LogsLength()) 761 childfb := &logfb.Log{} 762 for i := 0; i < lfb.LogsLength(); i++ { 763 if lfb.Logs(childfb, i) { 764 newLg.Logs[i] = &Log{} 765 newLg.Logs[i].UnmarshalFlatbuffer(childfb, lg) 766 newLg.Logs[i].ParentID = newLg.ID() 767 } 768 } 769 } 770 771 *lg = newLg 772 return nil 773 } 774 775 // OpType is the set of all kinds of operations, they are two bytes in length 776 // OpType splits the provided byte in half, using the higher 4 bits for the 777 // "category" of operation, and the lower 4 bits for the type of operation 778 // within the category 779 // the second byte is reserved for future use 780 type OpType byte 781 782 const ( 783 // OpTypeInit is the creation of a model 784 OpTypeInit OpType = 0x01 785 // OpTypeAmend represents amending a model 786 OpTypeAmend OpType = 0x02 787 // OpTypeRemove represents deleting a model 788 OpTypeRemove OpType = 0x03 789 ) 790 791 // Op is an operation, a single atomic unit in a log that describes a state 792 // change 793 type Op struct { 794 Type OpType // type of operation 795 Model uint32 // data model to operate on 796 Ref string // identifier of data this operation is documenting 797 Prev string // previous reference in a causal history 798 Relations []string // references this operation relates to. usage is operation type-dependant 799 Name string // human-readable name for the reference 800 AuthorID string // identifier for author 801 802 Timestamp int64 // operation timestamp, for annotation purposes only 803 Size int64 // size of the referenced value in bytes 804 Note string // operation annotation for users. eg: commit title 805 } 806 807 // Equal tests equality between two operations 808 func (o Op) Equal(b Op) bool { 809 return o.Type == b.Type && 810 o.Model == b.Model && 811 o.Ref == b.Ref && 812 o.Prev == b.Prev && 813 len(o.Relations) == len(b.Relations) && 814 o.Name == b.Name && 815 o.AuthorID == b.AuthorID && 816 o.Timestamp == b.Timestamp && 817 o.Size == b.Size && 818 o.Note == b.Note 819 } 820 821 // Hash uses lower-case base32 encoding for id bytes for a few reasons: 822 // * base64 uses the "/" character, which messes with paths 823 // * can be used as URLS 824 // * doesn't rely on case, which means it works in case-insensitive contexts 825 // * lowercase is easier on the eyes 826 // 827 // we're intentionally *not* using multiformat CIDs here. ID's are not 828 // identifiers of content stored wholly in an immutable filesystem, they're a 829 // reference to the intialization operation in a history 830 var base32Enc = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding) 831 832 // Hash returns the base32-lowercase-encoded blake2b-256 hash of the Op flatbuffer 833 func (o Op) Hash() string { 834 builder := flatbuffers.NewBuilder(0) 835 end := o.MarshalFlatbuffer(builder) 836 builder.Finish(end) 837 data := builder.FinishedBytes() 838 sum := blake2b.Sum256(data) 839 return base32Enc.EncodeToString(sum[:]) 840 } 841 842 // MarshalFlatbuffer writes this operation to a flatbuffer, returning the 843 // ending byte offset 844 func (o Op) MarshalFlatbuffer(builder *flatbuffers.Builder) flatbuffers.UOffsetT { 845 ref := builder.CreateString(o.Ref) 846 prev := builder.CreateString(o.Prev) 847 name := builder.CreateString(o.Name) 848 authorID := builder.CreateString(o.AuthorID) 849 note := builder.CreateString(o.Note) 850 851 count := len(o.Relations) 852 offsets := make([]flatbuffers.UOffsetT, count) 853 for i, r := range o.Relations { 854 offsets[i] = builder.CreateString(r) 855 } 856 857 logfb.OperationStartRelationsVector(builder, count) 858 for i := count - 1; i >= 0; i-- { 859 builder.PrependUOffsetT(offsets[i]) 860 } 861 rels := builder.EndVector(count) 862 863 logfb.OperationStart(builder) 864 logfb.OperationAddType(builder, logfb.OpType(o.Type)) 865 logfb.OperationAddModel(builder, o.Model) 866 logfb.OperationAddRef(builder, ref) 867 logfb.OperationAddRelations(builder, rels) 868 logfb.OperationAddPrev(builder, prev) 869 logfb.OperationAddName(builder, name) 870 logfb.OperationAddAuthorID(builder, authorID) 871 logfb.OperationAddTimestamp(builder, o.Timestamp) 872 logfb.OperationAddSize(builder, o.Size) 873 logfb.OperationAddNote(builder, note) 874 return logfb.OperationEnd(builder) 875 } 876 877 // UnmarshalOpFlatbuffer creates an op from a flatbuffer operation pointer 878 func UnmarshalOpFlatbuffer(o *logfb.Operation) Op { 879 op := Op{ 880 Type: OpType(byte(o.Type())), 881 Model: o.Model(), 882 Timestamp: o.Timestamp(), 883 Ref: string(o.Ref()), 884 Prev: string(o.Prev()), 885 Name: string(o.Name()), 886 AuthorID: string(o.AuthorID()), 887 Size: o.Size(), 888 Note: string(o.Note()), 889 } 890 891 if o.RelationsLength() > 0 { 892 op.Relations = make([]string, o.RelationsLength()) 893 for i := 0; i < o.RelationsLength(); i++ { 894 op.Relations[i] = string(o.Relations(i)) 895 } 896 } 897 898 return op 899 }