1/* 2Copyright 2013 The Perkeep Authors 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17// Package blob defines types to refer to and retrieve low-level Perkeep blobs. 18package blob // import "perkeep.org/pkg/blob" 19 20import ( 21 "bytes" 22 "crypto/sha1" 23 "crypto/sha256" 24 "errors" 25 "fmt" 26 "hash" 27 "io" 28 "reflect" 29 "strings" 30 31 "perkeep.org/internal/testhooks" 32) 33 34// Pattern is the regular expression which matches a blobref. 35// It does not contain ^ or $. 36const Pattern = `\b([a-z][a-z0-9]*)-([a-f0-9]+)\b` 37 38// Ref is a reference to a Perkeep blob. 39// It is used as a value type and supports equality (with ==) and the ability 40// to use it as a map key. 41type Ref struct { 42 digest digestType 43} 44 45// SizedRef is like a Ref but includes a size. 46// It should also be used as a value type and supports equality. 47type SizedRef struct { 48 Ref Ref `json:"blobRef"` 49 Size uint32 `json:"size"` 50} 51 52// Less reports whether sr sorts before o. Invalid references blobs sort first. 53func (sr SizedRef) Less(o SizedRef) bool { 54 return sr.Ref.Less(o.Ref) 55} 56 57func (sr SizedRef) Valid() bool { return sr.Ref.Valid() } 58 59func (sr SizedRef) HashMatches(h hash.Hash) bool { return sr.Ref.HashMatches(h) } 60 61func (sr SizedRef) String() string { 62 return fmt.Sprintf("[%s; %d bytes]", sr.Ref.String(), sr.Size) 63} 64 65// digestType is an interface type, but any type implementing it must 66// be of concrete type [N]byte, so it supports equality with ==, 67// which is a requirement for ref. 68type digestType interface { 69 bytes() []byte 70 digestName() string 71 newHash() hash.Hash 72 equalString(string) bool 73 hasPrefix(string) bool 74} 75 76func (r Ref) String() string { 77 if r.digest == nil { 78 return "<invalid-blob.Ref>" 79 } 80 dname := r.digest.digestName() 81 bs := r.digest.bytes() 82 buf := getBuf(len(dname) + 1 + len(bs)*2)[:0] 83 defer putBuf(buf) 84 return string(r.appendString(buf)) 85} 86 87// StringMinusOne returns the first string that's before String. 88func (r Ref) StringMinusOne() string { 89 if r.digest == nil { 90 return "<invalid-blob.Ref>" 91 } 92 dname := r.digest.digestName() 93 bs := r.digest.bytes() 94 buf := getBuf(len(dname) + 1 + len(bs)*2)[:0] 95 defer putBuf(buf) 96 buf = r.appendString(buf) 97 buf[len(buf)-1]-- // no need to deal with carrying underflow (no 0 bytes ever) 98 return string(buf) 99} 100 101// EqualString reports whether r.String() is equal to s. 102// It does not allocate. 103func (r Ref) EqualString(s string) bool { return r.digest.equalString(s) } 104 105// HasPrefix reports whether s is a prefix of r.String(). It returns false if s 106// does not contain at least the digest name prefix (e.g. "sha224-") and one byte of 107// digest. 108// It does not allocate. 109func (r Ref) HasPrefix(s string) bool { return r.digest.hasPrefix(s) } 110 111func (r Ref) appendString(buf []byte) []byte { 112 dname := r.digest.digestName() 113 bs := r.digest.bytes() 114 buf = append(buf, dname...) 115 buf = append(buf, '-') 116 for _, b := range bs { 117 buf = append(buf, hexDigit[b>>4], hexDigit[b&0xf]) 118 } 119 if o, ok := r.digest.(otherDigest); ok && o.odd { 120 buf = buf[:len(buf)-1] 121 } 122 return buf 123} 124 125// HashName returns the lowercase hash function name of the reference. 126// It panics if r is zero. 127func (r Ref) HashName() string { 128 if r.digest == nil { 129 panic("HashName called on invalid Ref") 130 } 131 return r.digest.digestName() 132} 133 134// Digest returns the lower hex digest of the blobref, without 135// the e.g. "sha224-" prefix. It panics if r is zero. 136func (r Ref) Digest() string { 137 if r.digest == nil { 138 panic("Digest called on invalid Ref") 139 } 140 bs := r.digest.bytes() 141 buf := getBuf(len(bs) * 2)[:0] 142 defer putBuf(buf) 143 for _, b := range bs { 144 buf = append(buf, hexDigit[b>>4], hexDigit[b&0xf]) 145 } 146 if o, ok := r.digest.(otherDigest); ok && o.odd { 147 buf = buf[:len(buf)-1] 148 } 149 return string(buf) 150} 151 152func (r Ref) DigestPrefix(digits int) string { 153 v := r.Digest() 154 if len(v) < digits { 155 return v 156 } 157 return v[:digits] 158} 159 160func (r Ref) DomID() string { 161 if !r.Valid() { 162 return "" 163 } 164 return "camli-" + r.String() 165} 166 167func (r Ref) Sum32() uint32 { 168 var v uint32 169 for _, b := range r.digest.bytes()[:4] { 170 v = v<<8 | uint32(b) 171 } 172 return v 173} 174 175func (r Ref) Sum64() uint64 { 176 var v uint64 177 for _, b := range r.digest.bytes()[:8] { 178 v = v<<8 | uint64(b) 179 } 180 return v 181} 182 183// Hash returns a new hash.Hash of r's type. 184// It panics if r is zero. 185func (r Ref) Hash() hash.Hash { 186 return r.digest.newHash() 187} 188 189func (r Ref) HashMatches(h hash.Hash) bool { 190 if r.digest == nil { 191 return false 192 } 193 return bytes.Equal(h.Sum(nil), r.digest.bytes()) 194} 195 196const hexDigit = "0123456789abcdef" 197 198func (r Ref) Valid() bool { return r.digest != nil } 199 200func (r Ref) IsSupported() bool { 201 if !r.Valid() { 202 return false 203 } 204 _, ok := metaFromString[r.digest.digestName()] 205 return ok 206} 207 208// ParseKnown is like Parse, but only parse blobrefs known to this 209// server. It returns ok == false for well-formed but unsupported 210// blobrefs. 211func ParseKnown(s string) (ref Ref, ok bool) { 212 return parse(s, false) 213} 214 215// Parse parse s as a blobref and returns the ref and whether it was 216// parsed successfully. 217func Parse(s string) (ref Ref, ok bool) { 218 return parse(s, true) 219} 220 221func parse(s string, allowAll bool) (ref Ref, ok bool) { 222 i := strings.Index(s, "-") 223 if i < 0 { 224 return 225 } 226 name := s[:i] // e.g. "sha1", "sha224" 227 hex := s[i+1:] 228 meta, ok := metaFromString[name] 229 if !ok { 230 if allowAll || testRefType[name] { 231 return parseUnknown(name, hex) 232 } 233 return 234 } 235 if len(hex) != meta.size*2 { 236 ok = false 237 return 238 } 239 dt, ok := meta.ctors(hex) 240 if !ok { 241 return 242 } 243 return Ref{dt}, true 244} 245 246var testRefType = map[string]bool{ 247 "fakeref": true, 248 "testref": true, 249 "perma": true, 250} 251 252// ParseBytes is like Parse, but parses from a byte slice. 253func ParseBytes(s []byte) (ref Ref, ok bool) { 254 i := bytes.IndexByte(s, '-') 255 if i < 0 { 256 return 257 } 258 name := s[:i] // e.g. "sha1", "sha224" 259 hex := s[i+1:] 260 meta, ok := metaFromBytes(name) 261 if !ok { 262 return parseUnknown(string(name), string(hex)) 263 } 264 if len(hex) != meta.size*2 { 265 ok = false 266 return 267 } 268 dt, ok := meta.ctorb(hex) 269 if !ok { 270 return 271 } 272 return Ref{dt}, true 273} 274 275// ParseOrZero parses as a blobref. If s is invalid, a zero Ref is 276// returned which can be tested with the Valid method. 277func ParseOrZero(s string) Ref { 278 ref, ok := Parse(s) 279 if !ok { 280 return Ref{} 281 } 282 return ref 283} 284 285// MustParse parse s as a blobref and panics on failure. 286func MustParse(s string) Ref { 287 ref, ok := Parse(s) 288 if !ok { 289 panic("Invalid blobref " + s) 290 } 291 return ref 292} 293 294// '0' => 0 ... 'f' => 15, else sets *bad to true. 295func hexVal(b byte, bad *bool) byte { 296 if '0' <= b && b <= '9' { 297 return b - '0' 298 } 299 if 'a' <= b && b <= 'f' { 300 return b - 'a' + 10 301 } 302 *bad = true 303 return 0 304} 305 306func validDigestName(name string) bool { 307 if name == "" { 308 return false 309 } 310 for _, r := range name { 311 if 'a' <= r && r <= 'z' { 312 continue 313 } 314 if '0' <= r && r <= '9' { 315 continue 316 } 317 return false 318 } 319 return true 320} 321 322// parseUnknown parses a blobref where the digest type isn't known to this server. 323// e.g. ("foo-ababab") 324func parseUnknown(digest, hex string) (ref Ref, ok bool) { 325 if !validDigestName(digest) { 326 return 327 } 328 329 // TODO: remove this short hack and don't allow odd numbers of hex digits. 330 odd := false 331 if len(hex)%2 != 0 { 332 hex += "0" 333 odd = true 334 } 335 336 if len(hex) < 2 || len(hex)%2 != 0 || len(hex) > maxOtherDigestLen*2 { 337 return 338 } 339 o := otherDigest{ 340 name: digest, 341 sumLen: len(hex) / 2, 342 odd: odd, 343 } 344 bad := false 345 for i := 0; i < len(hex); i += 2 { 346 o.sum[i/2] = hexVal(hex[i], &bad)<<4 | hexVal(hex[i+1], &bad) 347 } 348 if bad { 349 return 350 } 351 return Ref{o}, true 352} 353 354func sha1FromBinary(b []byte) digestType { 355 var d sha1Digest 356 if len(d) != len(b) { 357 panic("bogus sha-1 length") 358 } 359 copy(d[:], b) 360 return d 361} 362 363func sha1FromHexString(hex string) (digestType, bool) { 364 var d sha1Digest 365 var bad bool 366 for i := 0; i < len(hex); i += 2 { 367 d[i/2] = hexVal(hex[i], &bad)<<4 | hexVal(hex[i+1], &bad) 368 } 369 if bad { 370 return nil, false 371 } 372 return d, true 373} 374 375// yawn. exact copy of sha1FromHexString. 376func sha1FromHexBytes(hex []byte) (digestType, bool) { 377 var d sha1Digest 378 var bad bool 379 for i := 0; i < len(hex); i += 2 { 380 d[i/2] = hexVal(hex[i], &bad)<<4 | hexVal(hex[i+1], &bad) 381 } 382 if bad { 383 return nil, false 384 } 385 return d, true 386} 387 388func sha224FromBinary(b []byte) digestType { 389 var d sha224Digest 390 if len(d) != len(b) { 391 panic("bogus sha-224 length") 392 } 393 copy(d[:], b) 394 return d 395} 396 397func sha224FromHexString(hex string) (digestType, bool) { 398 var d sha224Digest 399 var bad bool 400 for i := 0; i < len(hex); i += 2 { 401 d[i/2] = hexVal(hex[i], &bad)<<4 | hexVal(hex[i+1], &bad) 402 } 403 if bad { 404 return nil, false 405 } 406 return d, true 407} 408 409// yawn. exact copy of sha224FromHexString. 410func sha224FromHexBytes(hex []byte) (digestType, bool) { 411 var d sha224Digest 412 var bad bool 413 for i := 0; i < len(hex); i += 2 { 414 d[i/2] = hexVal(hex[i], &bad)<<4 | hexVal(hex[i+1], &bad) 415 } 416 if bad { 417 return nil, false 418 } 419 return d, true 420} 421 422// RefFromHash returns a blobref representing the given hash. 423// It panics if the hash isn't of a known type. 424func RefFromHash(h hash.Hash) Ref { 425 meta, ok := metaFromType[hashSig{reflect.TypeOf(h), h.Size()}] 426 if !ok { 427 panic(fmt.Sprintf("Currently-unsupported hash type %T", h)) 428 } 429 return Ref{meta.ctor(h.Sum(nil))} 430} 431 432// RefFromString returns a blobref from the given string, for the currently 433// recommended hash function. 434func RefFromString(s string) Ref { 435 h := NewHash() 436 io.WriteString(h, s) 437 return RefFromHash(h) 438} 439 440// RefFromBytes returns a blobref from the given string, for the currently 441// recommended hash function. 442func RefFromBytes(b []byte) Ref { 443 h := NewHash() 444 h.Write(b) 445 return RefFromHash(h) 446} 447 448type sha1Digest [20]byte 449 450func (d sha1Digest) digestName() string { return "sha1" } 451func (d sha1Digest) bytes() []byte { return d[:] } 452func (d sha1Digest) newHash() hash.Hash { return sha1.New() } 453func (d sha1Digest) equalString(s string) bool { 454 if len(s) != 45 { 455 return false 456 } 457 if !strings.HasPrefix(s, "sha1-") { 458 return false 459 } 460 s = s[len("sha1-"):] 461 for i, b := range d[:] { 462 if s[i*2] != hexDigit[b>>4] || s[i*2+1] != hexDigit[b&0xf] { 463 return false 464 } 465 } 466 return true 467} 468 469func (d sha1Digest) hasPrefix(s string) bool { 470 if len(s) > 45 { 471 return false 472 } 473 if len(s) == 45 { 474 return d.equalString(s) 475 } 476 if !strings.HasPrefix(s, "sha1-") { 477 return false 478 } 479 s = s[len("sha1-"):] 480 if len(s) == 0 { 481 // we want at least one digest char to match on 482 return false 483 } 484 for i, b := range d[:] { 485 even := i * 2 486 if even == len(s) { 487 break 488 } 489 if s[even] != hexDigit[b>>4] { 490 return false 491 } 492 odd := i*2 + 1 493 if odd == len(s) { 494 break 495 } 496 if s[odd] != hexDigit[b&0xf] { 497 return false 498 } 499 } 500 return true 501} 502 503type sha224Digest [28]byte 504 505const sha224StrLen = 63 // len("sha224-d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f") 506 507func (d sha224Digest) digestName() string { return "sha224" } 508func (d sha224Digest) bytes() []byte { return d[:] } 509func (d sha224Digest) newHash() hash.Hash { return sha256.New224() } 510func (d sha224Digest) equalString(s string) bool { 511 if len(s) != sha224StrLen { 512 return false 513 } 514 if !strings.HasPrefix(s, "sha224-") { 515 return false 516 } 517 s = s[len("sha224-"):] 518 for i, b := range d[:] { 519 if s[i*2] != hexDigit[b>>4] || s[i*2+1] != hexDigit[b&0xf] { 520 return false 521 } 522 } 523 return true 524} 525 526func (d sha224Digest) hasPrefix(s string) bool { 527 if len(s) > sha224StrLen { 528 return false 529 } 530 if len(s) == sha224StrLen { 531 return d.equalString(s) 532 } 533 if !strings.HasPrefix(s, "sha224-") { 534 return false 535 } 536 s = s[len("sha224-"):] 537 if len(s) == 0 { 538 // we want at least one digest char to match on 539 return false 540 } 541 for i, b := range d[:] { 542 even := i * 2 543 if even == len(s) { 544 break 545 } 546 if s[even] != hexDigit[b>>4] { 547 return false 548 } 549 odd := i*2 + 1 550 if odd == len(s) { 551 break 552 } 553 if s[odd] != hexDigit[b&0xf] { 554 return false 555 } 556 } 557 return true 558} 559 560const maxOtherDigestLen = 128 561 562type otherDigest struct { 563 name string 564 sum [maxOtherDigestLen]byte 565 sumLen int // bytes in sum that are valid 566 odd bool // odd number of hex digits in input 567} 568 569func (d otherDigest) digestName() string { return d.name } 570func (d otherDigest) bytes() []byte { return d.sum[:d.sumLen] } 571func (d otherDigest) newHash() hash.Hash { return nil } 572func (d otherDigest) equalString(s string) bool { 573 wantLen := len(d.name) + len("-") + 2*d.sumLen 574 if d.odd { 575 wantLen-- 576 } 577 if len(s) != wantLen || !strings.HasPrefix(s, d.name) || s[len(d.name)] != '-' { 578 return false 579 } 580 s = s[len(d.name)+1:] 581 for i, b := range d.sum[:d.sumLen] { 582 if s[i*2] != hexDigit[b>>4] { 583 return false 584 } 585 if i == d.sumLen-1 && d.odd { 586 break 587 } 588 if s[i*2+1] != hexDigit[b&0xf] { 589 return false 590 } 591 } 592 return true 593} 594 595func (d otherDigest) hasPrefix(s string) bool { 596 maxLen := len(d.name) + len("-") + 2*d.sumLen 597 if d.odd { 598 maxLen-- 599 } 600 if len(s) > maxLen || !strings.HasPrefix(s, d.name) || s[len(d.name)] != '-' { 601 return false 602 } 603 if len(s) == maxLen { 604 return d.equalString(s) 605 } 606 s = s[len(d.name)+1:] 607 if len(s) == 0 { 608 // we want at least one digest char to match on 609 return false 610 } 611 for i, b := range d.sum[:d.sumLen] { 612 even := i * 2 613 if even == len(s) { 614 break 615 } 616 if s[even] != hexDigit[b>>4] { 617 return false 618 } 619 odd := i*2 + 1 620 if odd == len(s) { 621 break 622 } 623 if i == d.sumLen-1 && d.odd { 624 break 625 } 626 if s[odd] != hexDigit[b&0xf] { 627 return false 628 } 629 } 630 return true 631} 632 633var ( 634 sha1Meta = &digestMeta{ 635 ctor: sha1FromBinary, 636 ctors: sha1FromHexString, 637 ctorb: sha1FromHexBytes, 638 size: sha1.Size, 639 } 640 sha224Meta = &digestMeta{ 641 ctor: sha224FromBinary, 642 ctors: sha224FromHexString, 643 ctorb: sha224FromHexBytes, 644 size: sha256.Size224, 645 } 646) 647 648var metaFromString = map[string]*digestMeta{ 649 "sha1": sha1Meta, 650 "sha224": sha224Meta, 651} 652 653type blobTypeAndMeta struct { 654 name []byte 655 meta *digestMeta 656} 657 658var metas []blobTypeAndMeta 659 660func metaFromBytes(name []byte) (meta *digestMeta, ok bool) { 661 for _, bm := range metas { 662 if bytes.Equal(name, bm.name) { 663 return bm.meta, true 664 } 665 } 666 return 667} 668 669func init() { 670 for name, meta := range metaFromString { 671 metas = append(metas, blobTypeAndMeta{ 672 name: []byte(name), 673 meta: meta, 674 }) 675 } 676} 677 678// HashFuncs returns the names of the supported hash functions. 679func HashFuncs() []string { 680 hashes := make([]string, len(metas)) 681 for i, m := range metas { 682 hashes[i] = string(m.name) 683 } 684 return hashes 685} 686 687var ( 688 sha1Type = reflect.TypeOf(sha1.New()) 689 sha224Type = reflect.TypeOf(sha256.New224()) 690) 691 692// hashSig is the tuple (reflect.Type, hash size), for use as a map key. 693// The size disambiguates SHA-256 vs SHA-224, both of which have the same 694// reflect.Type (crypto/sha256.digest, but one has is224 bool set true). 695type hashSig struct { 696 rt reflect.Type 697 size int 698} 699 700var metaFromType = map[hashSig]*digestMeta{ 701 {sha1Type, sha1.Size}: sha1Meta, 702 {sha224Type, sha256.Size224}: sha224Meta, 703} 704 705type digestMeta struct { 706 ctor func(binary []byte) digestType 707 ctors func(hex string) (digestType, bool) 708 ctorb func(hex []byte) (digestType, bool) 709 size int // bytes of digest 710} 711 712var bufPool = make(chan []byte, 20) 713 714func getBuf(size int) []byte { 715 for { 716 select { 717 case b := <-bufPool: 718 if cap(b) >= size { 719 return b[:size] 720 } 721 default: 722 return make([]byte, size) 723 } 724 } 725} 726 727func putBuf(b []byte) { 728 select { 729 case bufPool <- b: 730 default: 731 } 732} 733 734// NewHash returns a new hash.Hash of the currently recommended hash type. 735// Currently this is SHA-224, but is subject to change over time. 736func NewHash() hash.Hash { 737 if testhooks.UseSHA1() { 738 return sha1.New() 739 } 740 return sha256.New224() 741} 742 743func ValidRefString(s string) bool { 744 // TODO: optimize to not allocate 745 return ParseOrZero(s).Valid() 746} 747 748var null = []byte(`null`) 749 750func (r *Ref) UnmarshalJSON(d []byte) error { 751 if r.digest != nil { 752 return errors.New("Can't UnmarshalJSON into a non-zero Ref") 753 } 754 if len(d) == 0 || bytes.Equal(d, null) { 755 return nil 756 } 757 if len(d) < 2 || d[0] != '"' || d[len(d)-1] != '"' { 758 return fmt.Errorf("blob: expecting a JSON string to unmarshal, got %q", d) 759 } 760 d = d[1 : len(d)-1] 761 p, ok := ParseBytes(d) 762 if !ok { 763 return fmt.Errorf("blobref: invalid blobref %q (%d)", d, len(d)) 764 } 765 *r = p 766 return nil 767} 768 769func (r Ref) MarshalJSON() ([]byte, error) { 770 if !r.Valid() { 771 return null, nil 772 } 773 dname := r.digest.digestName() 774 bs := r.digest.bytes() 775 buf := make([]byte, 0, 3+len(dname)+len(bs)*2) 776 buf = append(buf, '"') 777 buf = r.appendString(buf) 778 buf = append(buf, '"') 779 return buf, nil 780} 781 782// MarshalBinary implements Go's encoding.BinaryMarshaler interface. 783func (r Ref) MarshalBinary() (data []byte, err error) { 784 dname := r.digest.digestName() 785 bs := r.digest.bytes() 786 data = make([]byte, 0, len(dname)+1+len(bs)) 787 data = append(data, dname...) 788 data = append(data, '-') 789 data = append(data, bs...) 790 return 791} 792 793// UnmarshalBinary implements Go's encoding.BinaryUnmarshaler interface. 794func (r *Ref) UnmarshalBinary(data []byte) error { 795 if r.digest != nil { 796 return errors.New("Can't UnmarshalBinary into a non-zero Ref") 797 } 798 i := bytes.IndexByte(data, '-') 799 if i < 1 { 800 return errors.New("no digest name") 801 } 802 803 digName := string(data[:i]) 804 buf := data[i+1:] 805 806 meta, ok := metaFromString[digName] 807 if !ok { 808 r2, ok := parseUnknown(digName, fmt.Sprintf("%x", buf)) 809 if !ok { 810 return errors.New("invalid blobref binary data") 811 } 812 *r = r2 813 return nil 814 } 815 if len(buf) != meta.size { 816 return errors.New("wrong size of data for digest " + digName) 817 } 818 r.digest = meta.ctor(buf) 819 return nil 820} 821 822// Less reports whether r sorts before o. Invalid references blobs sort first. 823func (r Ref) Less(o Ref) bool { 824 if r.Valid() != o.Valid() { 825 return o.Valid() 826 } 827 if !r.Valid() { 828 return false 829 } 830 if n1, n2 := r.digest.digestName(), o.digest.digestName(); n1 != n2 { 831 return n1 < n2 832 } 833 return bytes.Compare(r.digest.bytes(), o.digest.bytes()) < 0 834} 835 836// ByRef sorts blob references. 837type ByRef []Ref 838 839func (s ByRef) Len() int { return len(s) } 840func (s ByRef) Less(i, j int) bool { return s[i].Less(s[j]) } 841func (s ByRef) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 842 843// SizedByRef sorts SizedRefs by their blobref. 844type SizedByRef []SizedRef 845 846func (s SizedByRef) Len() int { return len(s) } 847func (s SizedByRef) Less(i, j int) bool { return s[i].Less(s[j]) } 848func (s SizedByRef) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 849 850// TypeAlphabet returns the valid characters in the given blobref type. 851// It returns the empty string if the typ is unknown. 852func TypeAlphabet(typ string) string { 853 switch typ { 854 case "sha1": 855 return hexDigit 856 case "sha224": 857 return hexDigit 858 } 859 return "" 860} 861