wbxml

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2026 License: MIT Imports: 5 Imported by: 0

README

wbxml — WAP Binary XML codec for EAS

Go Reference

WBXML 1.3 encoder/decoder profiled for Microsoft Exchange ActiveSync (MS-ASWBXML). Stdlib only; no external dependencies.

This is the wire-format layer the eas package is built on. Most users won't touch it directly — but if you're debugging a bad request, adding a new EAS code page, or building a non-EAS WBXML client, this is the entry point.

Install

go get github.com/hstern/go-activesync/wbxml

Quick start

import "github.com/hstern/go-activesync/wbxml"

// Build a request body: <FolderSync><SyncKey>0</SyncKey></FolderSync>
doc := &wbxml.Document{
    Root: wbxml.E(wbxml.PageFolderHierarchy, "FolderSync",
        wbxml.E(wbxml.PageFolderHierarchy, "SyncKey", wbxml.Text("0")),
    ),
}

bytes, err := wbxml.Marshal(doc, wbxml.DefaultRegistry())
// → 0x03 0x01 0x6A 0x00  0x00 0x07  0x56  0x52 0x03 '0' 0x00 0x01  0x01

// Decode a server response back into the same tree shape.
parsed, err := wbxml.Unmarshal(bytes, wbxml.DefaultRegistry())
key := parsed.Root.Find("SyncKey").TextContent() // "0"

Marshal and Unmarshal round-trip byte-equal for any input the codec can produce — verified by the test suite with hand-crafted fixtures.

Wire format

Every WBXML document is a 4-byte header followed by a body of tag tokens, content tokens (STR_I for inline text, OPAQUE for raw bytes), and END markers terminating each element with content. A SWITCH_PAGE token (0x00 + page byte) changes the active code page mid-stream so subsequent tag bytes are looked up against the new page.

WBXML stream layout

Tag byte anatomy

A tag byte packs three things: an attribute flag (bit 7), a content flag (bit 6), and the tag's 6-bit identity within the active code page. Identities live in 0x05..0x3F because 0x00..0x04 are reserved for global tokens (SWITCH_PAGE, END, STR_I, etc.).

EAS doesn't use WBXML attributes, so the codec rejects any tag with the A-flag set rather than silently dropping the attribute payload.

Tag byte anatomy

Code pages

EAS partitions its tags into 25 numbered "code pages" — one per namespace (AirSync, Email, Calendar, AirSyncBase, Provision, …). The package ships them all preconfigured:

r := wbxml.DefaultRegistry()
r.PageName(wbxml.PageEmail)              // "Email"
name, _ := r.TagName(wbxml.PageEmail, 0x14)   // "Subject"
id, _   := r.TagID(wbxml.PageEmail, "From")   // 0x18

To add a custom or extended page, build your own registry:

r := wbxml.NewRegistry()
r.Add(&wbxml.Codepage{
    Number: 99,
    Name:   "MyCustom",
    Tags:   map[byte]string{0x05: "Hello", 0x06: "World"},
})
// pass r to Marshal / Unmarshal in place of DefaultRegistry()
Page Constant Notes
0 PageAirSync Sync command framing
1 PageContacts Contact items
2 PageEmail Email items
4 PageCalendar Calendar items
5 PageMove MoveItems
6 PageGetItemEstimate GetItemEstimate
7 PageFolderHierarchy FolderSync / FolderCreate / etc.
8 PageMeetingResponse Meeting accept/decline
9 PageTasks Task items
10 PageResolveRecipients Address resolution
11 PageValidateCert S/MIME cert validation
12 PageContacts2 Contact extras (NickName, IM, …)
13 PagePing Long-poll change notifications
14 PageProvision Policy negotiation
15 PageSearch Free-text + structured queries
16 PageGAL GAL search results
17 PageAirSyncBase Body / Attachments shared types
18 PageSettings OOF, DeviceInformation, …
19 PageDocumentLibrary SharePoint browsing (legacy)
20 PageItemOperations Fetch / Move / Empty
21 PageComposeMail SendMail / SmartReply / SmartForward
22 PageEmail2 Email extras (ConversationId, Bcc, …)
23 PageNotes Note items
24 PageRightsManagement IRM templates + license
25 PageFind EAS 16.x advanced search

Walking parsed trees

// Find: depth-first, returns the first matching element by name.
status := doc.Root.Find("Status")
if status != nil {
    code := status.TextContent()
}

// FindAll: every match in document order.
for _, add := range doc.Root.FindAll("Add") {
    serverID := add.Find("ServerId").TextContent()
}

// TextContent: concatenate inline Text children of one element.
subject := app.Find("Subject").TextContent()

Find ignores the code page and matches on tag name only — EAS tag names are unique enough across the spec that this is safe in practice (and far more ergonomic than matching (page, name) tuples). When ambiguity bites you, walk Element.Children directly and type-switch.

Building documents: wbxml.E

E(page, name, children...) is the one-liner for building element trees, designed to read top-down like the XML it produces:

wbxml.E(wbxml.PageAirSync, "Sync",
    wbxml.E(wbxml.PageAirSync, "Collections",
        wbxml.E(wbxml.PageAirSync, "Collection",
            wbxml.E(wbxml.PageAirSync, "SyncKey", wbxml.Text(key)),
            wbxml.E(wbxml.PageAirSync, "CollectionId", wbxml.Text(folderID)),
            wbxml.E(wbxml.PageAirSync, "GetChanges", wbxml.Text("1")),
            wbxml.E(wbxml.PageAirSync, "Options",
                wbxml.E(wbxml.PageAirSync, "FilterType", wbxml.Text("4")),
                wbxml.E(wbxml.PageAirSyncBase, "BodyPreference",
                    wbxml.E(wbxml.PageAirSyncBase, "Type", wbxml.Text("1")),
                    wbxml.E(wbxml.PageAirSyncBase, "TruncationSize", wbxml.Text("4096")),
                ),
            ),
        ),
    ),
)

The encoder emits SWITCH_PAGE tokens automatically at the seams between code pages.

Content node types

Three node types implement the Node interface:

Type What it encodes to Used for
*Element recursive (tag byte + children + END) nested structure
Text(s) STR_I 0x03 + UTF-8 bytes + 0x00 inline strings (sync keys, status codes, addresses)
Opaque(b) OPAQUE 0xC3 + mb_u_int32 length + raw bytes MIME bodies, S/MIME blobs, contact pictures, certificates — anything binary or NUL-bearing

Inline strings cannot contain NUL bytes (the encoder rejects them with a clear error rather than producing a truncated frame the server would silently misinterpret). Use Opaque for any byte sequence that might include NULs.

Errors

The decoder reports failures with byte offsets so fixtures can be debugged against the wire:

wbxml: unknown tag 0x11 on page 0 (AirSync) at offset 5
wbxml: element with attributes (byte 0xD6) at offset 6: not supported (EAS does not use attributes)
wbxml: mb_u_int32 longer than 5 bytes at offset 12
wbxml: element "Body": str_i: unexpected EOF

Most errors come from a registry/wire mismatch (a server returned a tag the registry doesn't know) or a truncated stream.

Status

Round-trip-tested against captured Z-Push and SOGo responses. Used in production by the eas package.

See also

Documentation

Overview

Package wbxml encodes and decodes the WAP Binary XML (WBXML 1.3) format as profiled by Microsoft Exchange ActiveSync (MS-ASWBXML).

EAS uses WBXML for the body of every command except Ping and OPTIONS. Each EAS namespace lives in a numbered "code page"; tag names are encoded as one-byte tokens whose meaning depends on the active code page. The SWITCH_PAGE control token changes the active page mid-stream.

This package handles encoding (Marshal) and decoding (Unmarshal) of documents and exposes a Registry of named code pages so that callers can add or replace pages as the EAS spec evolves. EAS does not use WBXML attributes, processing instructions, the LITERAL family, or string-table references, so the codec deliberately omits support for them — they would be dead code.

More

See README.md in this directory for the wire-format diagrams, the full code-page table, and tree-walking examples. Or browse the full godoc.

Index

Examples

Constants

View Source
const (
	PageAirSync           byte = 0
	PageContacts          byte = 1
	PageEmail             byte = 2
	PageCalendar          byte = 4
	PageMove              byte = 5
	PageGetItemEstimate   byte = 6
	PageFolderHierarchy   byte = 7
	PageMeetingResponse   byte = 8
	PageTasks             byte = 9
	PageResolveRecipients byte = 10
	PageValidateCert      byte = 11
	PageContacts2         byte = 12
	PagePing              byte = 13
	PageProvision         byte = 14
	PageSearch            byte = 15
	PageGAL               byte = 16
	PageAirSyncBase       byte = 17
	PageSettings          byte = 18
	PageDocumentLibrary   byte = 19
	PageItemOperations    byte = 20
	PageComposeMail       byte = 21
	PageEmail2            byte = 22
	PageNotes             byte = 23
	PageRightsManagement  byte = 24
	PageFind              byte = 25
)

EAS code page numbers.

Variables

This section is empty.

Functions

func Marshal

func Marshal(d *Document, r *Registry) ([]byte, error)

Marshal serializes a Document to the WBXML byte stream the EAS server expects. The Registry must define every code page referenced by the tree.

Example

Marshal turns a Document tree into the raw WBXML byte stream an EAS server expects. Build the tree with the wbxml.E helper.

The 13-byte output decodes as:

03 01 6a 00  header: WBXML 1.3, public id 1, charset 0x6A (UTF-8), empty string table
00 07        SWITCH_PAGE to FolderHierarchy (page 7)
56           FolderSync (id 0x16) | content flag (0x40)
52           SyncKey (id 0x12) | content flag (0x40)
03 30 00     STR_I "0" terminated by NUL
01 01        END SyncKey, END FolderSync
package main

import (
	"fmt"

	"github.com/hstern/go-activesync/wbxml"
)

func main() {
	doc := &wbxml.Document{
		Root: wbxml.E(wbxml.PageFolderHierarchy, "FolderSync",
			wbxml.E(wbxml.PageFolderHierarchy, "SyncKey", wbxml.Text("0")),
		),
	}
	out, err := wbxml.Marshal(doc, wbxml.DefaultRegistry())
	if err != nil {
		panic(err)
	}
	fmt.Printf("% x\n", out)
}
Output:
03 01 6a 00 00 07 56 52 03 30 00 01 01

Types

type Codepage

type Codepage struct {
	// Number is the page identifier emitted in SWITCH_PAGE.
	Number byte
	// Name is a human-readable label used in error messages
	// ("AirSync", "Provision", ...).
	Name string
	// Tags maps tag identity (0x05..0x3F) → tag name.
	Tags map[byte]string
}

Codepage describes one EAS namespace and the tag tokens defined in it.

Tag identities live in 0x05..0x3F. Lower values (0x00..0x04) are reserved for global control tokens (SWITCH_PAGE, END, STR_I, etc.) and values above 0x3F are reserved for the C/A flag bits used to mark "has content" / "has attributes" on a tag byte.

type Document

type Document struct {
	// Version is the WBXML version byte (defaults to 0x03 = WBXML 1.3).
	Version byte
	// PublicID identifies the document type. EAS always uses 0x01 ("unknown,
	// look up via charset"). Any nonzero value is preserved on round-trip.
	PublicID uint32
	// Charset identifies the character encoding (defaults to 0x6A = UTF-8,
	// which is what EAS mandates).
	Charset uint32
	// Root is the document's single root element.
	Root *Element
}

Document is a complete WBXML document: header fields plus a single root element. Only Root is significant when constructing a request; the header fields default to their EAS-standard values during Marshal.

func Unmarshal

func Unmarshal(b []byte, r *Registry) (*Document, error)

Unmarshal parses a WBXML byte stream into a Document. The Registry must define every code page referenced by the stream — unknown tag tokens are reported with byte offsets so fixtures can be debugged against the wire.

Example

Unmarshal parses a WBXML byte stream back into a Document tree. Use Find / FindAll / TextContent to walk the result.

package main

import (
	"fmt"

	"github.com/hstern/go-activesync/wbxml"
)

func main() {
	// A minimal FolderSync response: <FolderSync><Status>1</Status>
	//                                <SyncKey>42</SyncKey></FolderSync>
	raw := []byte{
		0x03, 0x01, 0x6a, 0x00, // header
		0x00, 0x07, // SWITCH_PAGE FolderHierarchy
		0x56,                        // FolderSync (content)
		0x4c, 0x03, '1', 0x00, 0x01, // Status (content) STR_I "1" END
		0x52, 0x03, '4', '2', 0x00, 0x01, // SyncKey (content) STR_I "42" END
		0x01, // END FolderSync
	}
	doc, err := wbxml.Unmarshal(raw, wbxml.DefaultRegistry())
	if err != nil {
		panic(err)
	}
	fmt.Println("status:", doc.Root.Find("Status").TextContent())
	fmt.Println("synckey:", doc.Root.Find("SyncKey").TextContent())
}
Output:
status: 1
synckey: 42

type Element

type Element struct {
	// Codepage is the EAS code page the tag lives in (0 = AirSync, 7 =
	// FolderHierarchy, etc.). The encoder emits SWITCH_PAGE tokens when this
	// changes between sibling elements; the decoder records the active page
	// at decode time.
	Codepage byte
	// Name is the tag name (e.g. "Sync", "FolderSync").
	Name string
	// Children is the ordered list of child nodes.
	Children []Node
}

Element is a tagged node identified by (Codepage, Name).

func E

func E(page byte, name string, children ...Node) *Element

E constructs a new Element with the given page, name, and children. Convenience for building request documents in tests and command builders:

root := wbxml.E(wbxml.PageFolderHierarchy, "FolderSync",
    wbxml.E(wbxml.PageFolderHierarchy, "SyncKey", wbxml.Text("0")),
)

func (*Element) Find

func (e *Element) Find(name string) *Element

Find returns the first descendant element with the given name, or nil. Search is depth-first, pre-order. Codepage is not matched (EAS tag names are unique within the spec given the relevant page context).

Example

Find walks descendants depth-first by tag name. Use FindAll for every match in document order, or iterate Element.Children directly when the same name is ambiguous across code pages.

package main

import (
	"fmt"

	"github.com/hstern/go-activesync/wbxml"
)

func main() {
	root := wbxml.E(wbxml.PageAirSync, "Sync",
		wbxml.E(wbxml.PageAirSync, "Collections",
			wbxml.E(wbxml.PageAirSync, "Collection",
				wbxml.E(wbxml.PageAirSync, "SyncKey", wbxml.Text("abc")),
				wbxml.E(wbxml.PageAirSync, "CollectionId", wbxml.Text("inbox")),
			),
		),
	)
	fmt.Println(root.Find("CollectionId").TextContent())
}
Output:
inbox

func (*Element) FindAll

func (e *Element) FindAll(name string) []*Element

FindAll returns all descendant elements with the given name in document order (pre-order traversal).

func (*Element) TextContent

func (e *Element) TextContent() string

TextContent returns the concatenation of all top-level Text children. Most EAS scalar elements (status codes, sync keys, IDs) use a single Text child; this helper avoids open-coding the type assertion at every call site.

type Node

type Node interface {
	// contains filtered or unexported methods
}

Node is anything that may appear as a child of an Element: another Element, Text content, or Opaque (raw byte) content.

EAS rarely mixes these inside the same parent — most elements have either child elements, a single Text child, or a single Opaque child — but the encoding allows them to be freely interleaved and the decoder preserves whatever it sees.

type Opaque

type Opaque []byte

Opaque is raw byte content (encoded as the OPAQUE token in WBXML). EAS uses opaque content for MIME bodies in AirSyncBase elements.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry maps (page, identity) ↔ tag name. A registry is required for both encode (tag name → identity) and decode (identity → tag name).

Registry is not safe for concurrent mutation; populate it at startup and then treat it as read-only.

func DefaultRegistry

func DefaultRegistry() *Registry

DefaultRegistry returns a Registry populated with all code pages currently implemented.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

Example

NewRegistry plus Codepage lets callers add namespaces beyond the 25 EAS code pages DefaultRegistry ships with — e.g. for a vendor extension or a non-EAS WBXML profile.

package main

import (
	"fmt"

	"github.com/hstern/go-activesync/wbxml"
)

func main() {
	r := wbxml.NewRegistry()
	r.Add(&wbxml.Codepage{
		Number: 99,
		Name:   "MyApp",
		Tags: map[byte]string{
			0x05: "Hello",
			0x06: "World",
		},
	})

	name, _ := r.TagName(99, 0x05)
	id, _ := r.TagID(99, "World")
	fmt.Printf("0x05 → %s\n", name)
	fmt.Printf("World → 0x%02X\n", id)
}
Output:
0x05 → Hello
World → 0x06

func (*Registry) Add

func (r *Registry) Add(p *Codepage)

Add registers a code page. Panics if the page number is already registered, or if any tag identity is outside 0x05..0x3F.

func (*Registry) PageName

func (r *Registry) PageName(page byte) string

PageName returns the page's human-readable name (e.g. "AirSync") or "" if the page is unknown.

func (*Registry) TagID

func (r *Registry) TagID(page byte, name string) (byte, bool)

TagID returns the tag identity for (page, name) and ok=false if unknown.

func (*Registry) TagName

func (r *Registry) TagName(page, id byte) (string, bool)

TagName returns the tag name for (page, id) and ok=false if unknown.

type Text

type Text string

Text is inline UTF-8 string content (encoded as STR_I in WBXML).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL