github.com/acrespo/mobile@v0.0.0-20190107162257-dc0771356504/cmd/gomobile/binary_xml.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "encoding/xml" 9 "fmt" 10 "io" 11 "sort" 12 "strconv" 13 "strings" 14 "unicode/utf16" 15 ) 16 17 // binaryXML converts XML into Android's undocumented binary XML format. 18 // 19 // The best source of information on this format seems to be the source code 20 // in AOSP frameworks-base. Android "resource" types seem to describe the 21 // encoded bytes, in particular: 22 // 23 // ResChunk_header 24 // ResStringPool_header 25 // ResXMLTree_node 26 // 27 // These are defined in: 28 // 29 // https://android.googlesource.com/platform/frameworks/base/+/master/include/androidfw/ResourceTypes.h 30 // 31 // The rough format of the file is a resource chunk containing a sequence of 32 // chunks. Each chunk is made up of a header and a body. The header begins with 33 // the contents of the ResChunk_header struct, which includes the size of both 34 // the header and the body. 35 // 36 // Both the header and body are 4-byte aligned. 37 // 38 // Values are encoded as little-endian. 39 // 40 // The android source code for encoding is done in the aapt tool. Its source 41 // code lives in AOSP: 42 // 43 // https://android.googlesource.com/platform/frameworks/base.git/+/master/tools/aapt 44 // 45 // A sample layout: 46 // 47 // File Header (ResChunk_header, type XML) 48 // Chunk: String Pool (type STRING_POOL) 49 // Sequence of strings, each with the format: 50 // uint16 length 51 // uint16 extended_length -- only if top bit set on length 52 // UTF-16LE string 53 // two zero bytes 54 // Resource Map 55 // The [i]th 4-byte entry in the resource map corresponds with 56 // the [i]th string from the string pool. The 4-bytes are a 57 // Resource ID constant defined: 58 // http://developer.android.com/reference/android/R.attr.html 59 // This appears to be a way to map strings onto enum values. 60 // Chunk: Namespace Start (ResXMLTree_node; ResXMLTree_namespaceExt) 61 // Chunk: Element Start 62 // ResXMLTree_node 63 // ResXMLTree_attrExt 64 // ResXMLTree_attribute (repeated attributeCount times) 65 // Chunk: Element End 66 // (ResXMLTree_node; ResXMLTree_endElementExt) 67 // ... 68 // Chunk: Namespace End 69 func binaryXML(r io.Reader) ([]byte, error) { 70 lr := &lineReader{r: r} 71 d := xml.NewDecoder(lr) 72 73 pool := new(binStringPool) 74 depth := 0 75 elements := []chunk{} 76 namespaceEnds := make(map[int][]binEndNamespace) 77 78 for { 79 line := lr.line(d.InputOffset()) 80 tok, err := d.Token() 81 if err != nil { 82 if err == io.EOF { 83 break 84 } 85 return nil, err 86 } 87 switch tok := tok.(type) { 88 case xml.StartElement: 89 // uses-sdk is synthesized by the writer, disallow declaration in manifest. 90 if tok.Name.Local == "uses-sdk" { 91 return nil, fmt.Errorf("unsupported manifest tag <uses-sdk .../>") 92 } else if tok.Name.Local == "application" { 93 // synthesize <uses-sdk/> before handling <application> token 94 attr := xml.Attr{ 95 Name: xml.Name{ 96 Space: "http://schemas.android.com/apk/res/android", 97 Local: "minSdkVersion", 98 }, 99 Value: "15", 100 } 101 ba, err := pool.getAttr(attr) 102 if err != nil { 103 return nil, fmt.Errorf("failed to synthesize attr minSdkVersion=\"15\"") 104 } 105 elements = append(elements, 106 &binStartElement{ 107 line: line - 1, // current testing strategy is not friendly to synthesized tags, -1 for would-be location 108 ns: pool.getNS(""), 109 name: pool.get("uses-sdk"), 110 attr: []*binAttr{ba}, 111 }, 112 &binEndElement{ 113 line: line - 1, 114 ns: pool.getNS(""), 115 name: pool.get("uses-sdk"), 116 }) 117 } 118 // Intercept namespace definitions. 119 var attr []*binAttr 120 for _, a := range tok.Attr { 121 if a.Name.Space == "xmlns" { 122 elements = append(elements, binStartNamespace{ 123 line: line, 124 prefix: pool.get(a.Name.Local), 125 url: pool.get(a.Value), 126 }) 127 namespaceEnds[depth] = append([]binEndNamespace{{ 128 line: line, 129 prefix: pool.get(a.Name.Local), 130 url: pool.get(a.Value), 131 }}, namespaceEnds[depth]...) 132 continue 133 } 134 ba, err := pool.getAttr(a) 135 if err != nil { 136 return nil, fmt.Errorf("%d: %s: %v", line, a.Name.Local, err) 137 } 138 attr = append(attr, ba) 139 } 140 141 depth++ 142 elements = append(elements, &binStartElement{ 143 line: line, 144 ns: pool.getNS(tok.Name.Space), 145 name: pool.get(tok.Name.Local), 146 attr: attr, 147 }) 148 case xml.EndElement: 149 elements = append(elements, &binEndElement{ 150 line: line, 151 ns: pool.getNS(tok.Name.Space), 152 name: pool.get(tok.Name.Local), 153 }) 154 depth-- 155 if nsEnds := namespaceEnds[depth]; len(nsEnds) > 0 { 156 delete(namespaceEnds, depth) 157 for _, nsEnd := range nsEnds { 158 elements = append(elements, nsEnd) 159 } 160 } 161 case xml.CharData: 162 // The aapt tool appears to "compact" leading and 163 // trailing whitepsace. See XMLNode::removeWhitespace in 164 // https://android.googlesource.com/platform/frameworks/base.git/+/master/tools/aapt/XMLNode.cpp 165 if len(tok) == 0 { 166 continue 167 } 168 start, end := 0, len(tok) 169 for start < len(tok) && isSpace(tok[start]) { 170 start++ 171 } 172 for end > start && isSpace(tok[end-1]) { 173 end-- 174 } 175 if start == end { 176 continue // all whitespace, skip it 177 } 178 179 // Preserve one character of whitespace. 180 if start > 0 { 181 start-- 182 } 183 if end < len(tok) { 184 end++ 185 } 186 187 elements = append(elements, &binCharData{ 188 line: line, 189 data: pool.get(string(tok[start:end])), 190 }) 191 case xml.Comment: 192 // Ignored by Anroid Binary XML format. 193 case xml.ProcInst: 194 // Ignored by Anroid Binary XML format? 195 case xml.Directive: 196 // Ignored by Anroid Binary XML format. 197 default: 198 return nil, fmt.Errorf("apk: unexpected token: %v (%T)", tok, tok) 199 } 200 } 201 202 sortPool(pool) 203 for _, e := range elements { 204 if e, ok := e.(*binStartElement); ok { 205 sortAttr(e, pool) 206 } 207 } 208 209 resMap := &binResMap{pool} 210 211 size := 8 + pool.size() + resMap.size() 212 for _, e := range elements { 213 size += e.size() 214 } 215 216 b := make([]byte, 0, size) 217 b = appendHeader(b, headerXML, size) 218 b = pool.append(b) 219 b = resMap.append(b) 220 for _, e := range elements { 221 b = e.append(b) 222 } 223 224 return b, nil 225 } 226 227 func isSpace(b byte) bool { 228 switch b { 229 case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0: 230 return true 231 } 232 return false 233 } 234 235 type headerType uint16 236 237 const ( 238 headerXML headerType = 0x0003 239 headerStringPool = 0x0001 240 headerResourceMap = 0x0180 241 headerStartNamespace = 0x0100 242 headerEndNamespace = 0x0101 243 headerStartElement = 0x0102 244 headerEndElement = 0x0103 245 headerCharData = 0x0104 246 ) 247 248 func appendU16(b []byte, v uint16) []byte { 249 return append(b, byte(v), byte(v>>8)) 250 } 251 252 func appendU32(b []byte, v uint32) []byte { 253 return append(b, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)) 254 } 255 256 func appendHeader(b []byte, typ headerType, size int) []byte { 257 b = appendU16(b, uint16(typ)) 258 b = appendU16(b, 8) 259 b = appendU16(b, uint16(size)) 260 b = appendU16(b, 0) 261 return b 262 } 263 264 // Attributes of the form android:key are mapped to resource IDs, which are 265 // embedded into the Binary XML format. 266 // 267 // http://developer.android.com/reference/android/R.attr.html 268 var resourceCodes = map[string]uint32{ 269 "versionCode": 0x0101021b, 270 "versionName": 0x0101021c, 271 "minSdkVersion": 0x0101020c, 272 "windowFullscreen": 0x0101020d, 273 "theme": 0x01010000, 274 "label": 0x01010001, 275 "hasCode": 0x0101000c, 276 "debuggable": 0x0101000f, 277 "name": 0x01010003, 278 "screenOrientation": 0x0101001e, 279 "configChanges": 0x0101001f, 280 "value": 0x01010024, 281 } 282 283 // http://developer.android.com/reference/android/R.attr.html#configChanges 284 var configChanges = map[string]uint32{ 285 "mcc": 0x0001, 286 "mnc": 0x0002, 287 "locale": 0x0004, 288 "touchscreen": 0x0008, 289 "keyboard": 0x0010, 290 "keyboardHidden": 0x0020, 291 "navigation": 0x0040, 292 "orientation": 0x0080, 293 "screenLayout": 0x0100, 294 "uiMode": 0x0200, 295 "screenSize": 0x0400, 296 "smallestScreenSize": 0x0800, 297 "layoutDirection": 0x2000, 298 "fontScale": 0x40000000, 299 } 300 301 // http://developer.android.com/reference/android/R.attr.html#screenOrientation 302 var screenOrientation = map[string]int{ 303 "unspecified": -1, 304 "landscape": 0, 305 "portrait": 1, 306 "user": 2, 307 "behind": 3, 308 "sensor": 4, 309 "nosensor": 5, 310 "sensorLandscape": 6, 311 "sensorPortrait": 7, 312 "reverseLandscape": 8, 313 "reversePortrait": 9, 314 "fullSensor": 10, 315 "userLandscape": 11, 316 "userPortrait": 12, 317 "fullUser": 13, 318 "locked": 14, 319 } 320 321 // reference is an alias used to write out correct type in bin. 322 type reference uint32 323 324 // http://developer.android.com/reference/android/R.style.html 325 var theme = map[string]reference{ 326 "Theme": 0x01030005, 327 "Theme_NoTitleBar": 0x01030006, 328 "Theme_NoTitleBar_Fullscreen": 0x01030007, 329 "Theme_Black": 0x01030008, 330 "Theme_Black_NoTitleBar": 0x01030009, 331 "Theme_Black_NoTitleBar_Fullscreen": 0x0103000a, 332 "Theme_Light": 0x0103000c, 333 "Theme_Light_NoTitleBar": 0x0103000d, 334 "Theme_Light_NoTitleBar_Fullscreen": 0x0103000e, 335 "Theme_Translucent": 0x0103000f, 336 "Theme_Translucent_NoTitleBar": 0x01030010, 337 "Theme_Translucent_NoTitleBar_Fullscreen": 0x01030011, 338 } 339 340 type lineReader struct { 341 off int64 342 lines []int64 343 r io.Reader 344 } 345 346 func (r *lineReader) Read(p []byte) (n int, err error) { 347 n, err = r.r.Read(p) 348 for i := 0; i < n; i++ { 349 if p[i] == '\n' { 350 r.lines = append(r.lines, r.off+int64(i)) 351 } 352 } 353 r.off += int64(n) 354 return n, err 355 } 356 357 func (r *lineReader) line(pos int64) int { 358 return sort.Search(len(r.lines), func(i int) bool { 359 return pos < r.lines[i] 360 }) + 1 361 } 362 363 type bstring struct { 364 ind uint32 365 str string 366 enc []byte // 2-byte length, utf16le, 2-byte zero 367 } 368 369 type chunk interface { 370 size() int 371 append([]byte) []byte 372 } 373 374 type binResMap struct { 375 pool *binStringPool 376 } 377 378 func (p *binResMap) append(b []byte) []byte { 379 b = appendHeader(b, headerResourceMap, p.size()) 380 for _, bstr := range p.pool.s { 381 c, ok := resourceCodes[bstr.str] 382 if !ok { 383 break 384 } 385 b = appendU32(b, c) 386 } 387 return b 388 } 389 390 func (p *binResMap) size() int { 391 count := 0 392 for _, bstr := range p.pool.s { 393 if _, ok := resourceCodes[bstr.str]; !ok { 394 break 395 } 396 count++ 397 } 398 return 8 + 4*count 399 } 400 401 type binStringPool struct { 402 s []*bstring 403 m map[string]*bstring 404 } 405 406 func (p *binStringPool) get(str string) *bstring { 407 if p.m == nil { 408 p.m = make(map[string]*bstring) 409 } 410 res := p.m[str] 411 if res != nil { 412 return res 413 } 414 res = &bstring{ 415 ind: uint32(len(p.s)), 416 str: str, 417 } 418 p.s = append(p.s, res) 419 p.m[str] = res 420 421 if len(str)>>16 > 0 { 422 panic(fmt.Sprintf("string lengths over 1<<15 not yet supported, got len %d for string that starts %q", len(str), str[:100])) 423 } 424 strUTF16 := utf16.Encode([]rune(str)) 425 res.enc = appendU16(nil, uint16(len(strUTF16))) 426 for _, w := range strUTF16 { 427 res.enc = appendU16(res.enc, w) 428 } 429 res.enc = appendU16(res.enc, 0) 430 return res 431 } 432 433 func (p *binStringPool) getNS(ns string) *bstring { 434 if ns == "" { 435 // Register empty string for inclusion in output (like aapt), 436 // but do not reference it from namespace elements. 437 p.get("") 438 return nil 439 } 440 return p.get(ns) 441 } 442 443 func (p *binStringPool) getAttr(attr xml.Attr) (*binAttr, error) { 444 a := &binAttr{ 445 ns: p.getNS(attr.Name.Space), 446 name: p.get(attr.Name.Local), 447 } 448 if attr.Name.Space != "http://schemas.android.com/apk/res/android" { 449 a.data = p.get(attr.Value) 450 return a, nil 451 } 452 453 // Some android attributes have interesting values. 454 switch attr.Name.Local { 455 case "versionCode", "minSdkVersion": 456 v, err := strconv.Atoi(attr.Value) 457 if err != nil { 458 return nil, err 459 } 460 a.data = int(v) 461 case "hasCode", "debuggable": 462 v, err := strconv.ParseBool(attr.Value) 463 if err != nil { 464 return nil, err 465 } 466 a.data = v 467 case "configChanges": 468 v := uint32(0) 469 for _, c := range strings.Split(attr.Value, "|") { 470 v |= configChanges[c] 471 } 472 a.data = v 473 case "screenOrientation": 474 v := 0 475 for _, c := range strings.Split(attr.Value, "|") { 476 v |= screenOrientation[c] 477 } 478 a.data = v 479 case "theme": 480 v := attr.Value 481 // strip prefix if present as only platform themes are supported 482 if idx := strings.Index(attr.Value, "/"); idx != -1 { 483 v = v[idx+1:] 484 } 485 v = strings.Replace(v, ".", "_", -1) 486 a.data = theme[v] 487 default: 488 a.data = p.get(attr.Value) 489 } 490 return a, nil 491 } 492 493 const stringPoolPreamble = 0 + 494 8 + // chunk header 495 4 + // string count 496 4 + // style count 497 4 + // flags 498 4 + // strings start 499 4 + // styles start 500 0 501 502 func (p *binStringPool) unpaddedSize() int { 503 strLens := 0 504 for _, s := range p.s { 505 strLens += len(s.enc) 506 } 507 return stringPoolPreamble + 4*len(p.s) + strLens 508 } 509 510 func (p *binStringPool) size() int { 511 size := p.unpaddedSize() 512 size += size % 0x04 513 return size 514 } 515 516 // overloaded for testing. 517 var ( 518 sortPool = func(p *binStringPool) { 519 sort.Sort(p) 520 521 // Move resourceCodes to the front. 522 s := make([]*bstring, 0) 523 m := make(map[string]*bstring) 524 for str := range resourceCodes { 525 bstr := p.m[str] 526 if bstr == nil { 527 continue 528 } 529 bstr.ind = uint32(len(s)) 530 s = append(s, bstr) 531 m[str] = bstr 532 delete(p.m, str) 533 } 534 for _, bstr := range p.m { 535 bstr.ind = uint32(len(s)) 536 s = append(s, bstr) 537 } 538 p.s = s 539 p.m = m 540 } 541 sortAttr = func(e *binStartElement, p *binStringPool) {} 542 ) 543 544 func (b *binStringPool) Len() int { return len(b.s) } 545 func (b *binStringPool) Less(i, j int) bool { return b.s[i].str < b.s[j].str } 546 func (b *binStringPool) Swap(i, j int) { 547 b.s[i], b.s[j] = b.s[j], b.s[i] 548 b.s[i].ind, b.s[j].ind = b.s[j].ind, b.s[i].ind 549 } 550 551 func (p *binStringPool) append(b []byte) []byte { 552 stringsStart := uint32(stringPoolPreamble + 4*len(p.s)) 553 b = appendU16(b, uint16(headerStringPool)) 554 b = appendU16(b, 0x1c) // chunk header size 555 b = appendU16(b, uint16(p.size())) 556 b = appendU16(b, 0) 557 b = appendU32(b, uint32(len(p.s))) 558 b = appendU32(b, 0) // style count 559 b = appendU32(b, 0) // flags 560 b = appendU32(b, stringsStart) 561 b = appendU32(b, 0) // styles start 562 563 off := 0 564 for _, bstr := range p.s { 565 b = appendU32(b, uint32(off)) 566 off += len(bstr.enc) 567 } 568 for _, bstr := range p.s { 569 b = append(b, bstr.enc...) 570 } 571 572 for i := p.unpaddedSize() % 0x04; i > 0; i-- { 573 b = append(b, 0) 574 } 575 return b 576 } 577 578 type binStartElement struct { 579 line int 580 ns *bstring 581 name *bstring 582 attr []*binAttr 583 } 584 585 func (e *binStartElement) size() int { 586 return 8 + // chunk header 587 4 + // line number 588 4 + // comment 589 4 + // ns 590 4 + // name 591 2 + 2 + 2 + // attribute start, size, count 592 2 + 2 + 2 + // id/class/style index 593 len(e.attr)*(4+4+4+4+4) 594 } 595 596 func (e *binStartElement) append(b []byte) []byte { 597 b = appendU16(b, uint16(headerStartElement)) 598 b = appendU16(b, 0x10) // chunk header size 599 b = appendU16(b, uint16(e.size())) 600 b = appendU16(b, 0) 601 b = appendU32(b, uint32(e.line)) 602 b = appendU32(b, 0xffffffff) // comment 603 if e.ns == nil { 604 b = appendU32(b, 0xffffffff) 605 } else { 606 b = appendU32(b, e.ns.ind) 607 } 608 b = appendU32(b, e.name.ind) 609 b = appendU16(b, 0x14) // attribute start 610 b = appendU16(b, 0x14) // attribute size 611 b = appendU16(b, uint16(len(e.attr))) 612 b = appendU16(b, 0) // ID index (none) 613 b = appendU16(b, 0) // class index (none) 614 b = appendU16(b, 0) // style index (none) 615 for _, a := range e.attr { 616 b = a.append(b) 617 } 618 return b 619 } 620 621 type binAttr struct { 622 ns *bstring 623 name *bstring 624 data interface{} // either int (INT_DEC) or *bstring (STRING) 625 } 626 627 func (a *binAttr) append(b []byte) []byte { 628 if a.ns != nil { 629 b = appendU32(b, a.ns.ind) 630 } else { 631 b = appendU32(b, 0xffffffff) 632 } 633 b = appendU32(b, a.name.ind) 634 switch v := a.data.(type) { 635 case int: 636 b = appendU32(b, 0xffffffff) // raw value 637 b = appendU16(b, 8) // size 638 b = append(b, 0) // unused padding 639 b = append(b, 0x10) // INT_DEC 640 b = appendU32(b, uint32(v)) 641 case bool: 642 b = appendU32(b, 0xffffffff) // raw value 643 b = appendU16(b, 8) // size 644 b = append(b, 0) // unused padding 645 b = append(b, 0x12) // INT_BOOLEAN 646 if v { 647 b = appendU32(b, 0xffffffff) 648 } else { 649 b = appendU32(b, 0) 650 } 651 case uint32: 652 b = appendU32(b, 0xffffffff) // raw value 653 b = appendU16(b, 8) // size 654 b = append(b, 0) // unused padding 655 b = append(b, 0x11) // INT_HEX 656 b = appendU32(b, uint32(v)) 657 case reference: 658 b = appendU32(b, 0xffffffff) // raw value 659 b = appendU16(b, 8) // size 660 b = append(b, 0) // unused padding 661 b = append(b, 0x01) // REFERENCE 662 b = appendU32(b, uint32(v)) 663 case *bstring: 664 b = appendU32(b, v.ind) // raw value 665 b = appendU16(b, 8) // size 666 b = append(b, 0) // unused padding 667 b = append(b, 0x03) // STRING 668 b = appendU32(b, v.ind) 669 default: 670 panic(fmt.Sprintf("unexpected attr type: %T (%v)", v, v)) 671 } 672 return b 673 } 674 675 type binEndElement struct { 676 line int 677 ns *bstring 678 name *bstring 679 attr []*binAttr 680 } 681 682 func (*binEndElement) size() int { 683 return 8 + // chunk header 684 4 + // line number 685 4 + // comment 686 4 + // ns 687 4 // name 688 } 689 690 func (e *binEndElement) append(b []byte) []byte { 691 b = appendU16(b, uint16(headerEndElement)) 692 b = appendU16(b, 0x10) // chunk header size 693 b = appendU16(b, uint16(e.size())) 694 b = appendU16(b, 0) 695 b = appendU32(b, uint32(e.line)) 696 b = appendU32(b, 0xffffffff) // comment 697 if e.ns == nil { 698 b = appendU32(b, 0xffffffff) 699 } else { 700 b = appendU32(b, e.ns.ind) 701 } 702 b = appendU32(b, e.name.ind) 703 return b 704 } 705 706 type binStartNamespace struct { 707 line int 708 prefix *bstring 709 url *bstring 710 } 711 712 func (binStartNamespace) size() int { 713 return 8 + // chunk header 714 4 + // line number 715 4 + // comment 716 4 + // prefix 717 4 // url 718 } 719 720 func (e binStartNamespace) append(b []byte) []byte { 721 b = appendU16(b, uint16(headerStartNamespace)) 722 b = appendU16(b, 0x10) // chunk header size 723 b = appendU16(b, uint16(e.size())) 724 b = appendU16(b, 0) 725 b = appendU32(b, uint32(e.line)) 726 b = appendU32(b, 0xffffffff) // comment 727 b = appendU32(b, e.prefix.ind) 728 b = appendU32(b, e.url.ind) 729 return b 730 } 731 732 type binEndNamespace struct { 733 line int 734 prefix *bstring 735 url *bstring 736 } 737 738 func (binEndNamespace) size() int { 739 return 8 + // chunk header 740 4 + // line number 741 4 + // comment 742 4 + // prefix 743 4 // url 744 } 745 746 func (e binEndNamespace) append(b []byte) []byte { 747 b = appendU16(b, uint16(headerEndNamespace)) 748 b = appendU16(b, 0x10) // chunk header size 749 b = appendU16(b, uint16(e.size())) 750 b = appendU16(b, 0) 751 b = appendU32(b, uint32(e.line)) 752 b = appendU32(b, 0xffffffff) // comment 753 b = appendU32(b, e.prefix.ind) 754 b = appendU32(b, e.url.ind) 755 return b 756 } 757 758 type binCharData struct { 759 line int 760 data *bstring 761 } 762 763 func (*binCharData) size() int { 764 return 8 + // chunk header 765 4 + // line number 766 4 + // comment 767 4 + // data 768 8 // junk 769 } 770 771 func (e *binCharData) append(b []byte) []byte { 772 b = appendU16(b, uint16(headerCharData)) 773 b = appendU16(b, 0x10) // chunk header size 774 b = appendU16(b, 0x1c) // size 775 b = appendU16(b, 0) 776 b = appendU32(b, uint32(e.line)) 777 b = appendU32(b, 0xffffffff) // comment 778 b = appendU32(b, e.data.ind) 779 b = appendU16(b, 0x08) 780 b = appendU16(b, 0) 781 b = appendU16(b, 0) 782 b = appendU16(b, 0) 783 return b 784 }