diff --git a/vmap/encoder_fast.go b/vmap/encoder_fast.go
new file mode 100644
index 0000000..589e00c
--- /dev/null
+++ b/vmap/encoder_fast.go
@@ -0,0 +1,398 @@
+package vmap
+
+import (
+ "strconv"
+)
+
+// MarshalVmap marshals a VMAP to XML, producing output identical to encoding/xml.Marshal.
+func MarshalVmap(v *VMAP) ([]byte, error) {
+ buf := make([]byte, 0, 8192)
+ buf = appendVMAP(buf, v)
+ return buf, nil
+}
+
+// MarshalVast marshals a VAST to XML, producing output identical to encoding/xml.Marshal.
+func MarshalVast(v *VAST) ([]byte, error) {
+ buf := make([]byte, 0, 4096)
+ buf = appendVAST(buf, v)
+ return buf, nil
+}
+
+// --- escape helpers ---
+
+// escText escapes text content, matching encoding/xml.EscapeText.
+func escText(buf []byte, s string) []byte {
+ last := 0
+ for i := 0; i < len(s); i++ {
+ var esc string
+ switch s[i] {
+ case '&':
+ esc = "&"
+ case '<':
+ esc = "<"
+ case '>':
+ esc = ">"
+ case '"':
+ esc = """
+ case '\t':
+ esc = " "
+ case '\n':
+ esc = "
"
+ case '\r':
+ esc = "
"
+ default:
+ continue
+ }
+ buf = append(buf, s[last:i]...)
+ buf = append(buf, esc...)
+ last = i + 1
+ }
+ return append(buf, s[last:]...)
+}
+
+// escAttr escapes attribute values, matching encoding/xml attribute escaping.
+func escAttr(buf []byte, s string) []byte {
+ last := 0
+ for i := 0; i < len(s); i++ {
+ var esc string
+ switch s[i] {
+ case '&':
+ esc = "&"
+ case '<':
+ esc = "<"
+ case '>':
+ esc = ">"
+ case '"':
+ esc = """
+ case '\t':
+ esc = " "
+ case '\n':
+ esc = "
"
+ case '\r':
+ esc = "
"
+ default:
+ continue
+ }
+ buf = append(buf, s[last:i]...)
+ buf = append(buf, esc...)
+ last = i + 1
+ }
+ return append(buf, s[last:]...)
+}
+
+// --- duration / time offset helpers (allocation-free) ---
+
+func append2dig(buf []byte, n int) []byte {
+ return append(buf, byte('0'+n/10), byte('0'+n%10))
+}
+
+func append3dig(buf []byte, n int) []byte {
+ return append(buf, byte('0'+n/100), byte('0'+(n/10)%10), byte('0'+n%10))
+}
+
+func appendDuration(buf []byte, d Duration) []byte {
+ dur := d.Duration
+ if dur == 0 {
+ return append(buf, "00:00:00"...)
+ }
+ h := int(dur.Hours())
+ m := int(dur.Minutes()) % 60
+ s := int(dur.Seconds()) % 60
+ ms := int(dur.Milliseconds()) % 1000
+ buf = append2dig(buf, h)
+ buf = append(buf, ':')
+ buf = append2dig(buf, m)
+ buf = append(buf, ':')
+ buf = append2dig(buf, s)
+ if ms > 0 {
+ buf = append(buf, '.')
+ buf = append3dig(buf, ms)
+ }
+ return buf
+}
+
+func appendTimeOffset(buf []byte, to TimeOffset) []byte {
+ if to.Duration != nil {
+ return appendDuration(buf, *to.Duration)
+ }
+ if to.Position != 0 {
+ buf = append(buf, '#')
+ return strconv.AppendInt(buf, int64(to.Position), 10)
+ }
+ if to.Percent != 0 {
+ buf = strconv.AppendFloat(buf, float64(to.Percent*100), 'f', 6, 32)
+ return append(buf, '%')
+ }
+ return buf
+}
+
+// --- struct encoders ---
+// Field and attribute order matches encoding/xml.Marshal exactly.
+
+func appendVMAP(buf []byte, v *VMAP) []byte {
+ // XMLName tag is xml:"VMAP" (name only) — xml.Marshal does not output xmlns
+ buf = append(buf, `')
+
+ // chardata (Text field, before child elements, matching xml.Marshal field order)
+ buf = escText(buf, v.Text)
+
+ for i := range v.AdBreaks {
+ buf = appendAdBreak(buf, &v.AdBreaks[i])
+ }
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendAdBreak(buf []byte, ab *AdBreak) []byte {
+ // attrs: breakId, breakType, timeOffset
+ buf = append(buf, `')
+
+ // child elements in field order: AdSource, TrackingEvents
+ if ab.AdSource != nil {
+ buf = appendAdSource(buf, ab.AdSource)
+ }
+ // Wrapper always emitted for nested path xml:"TrackingEvents>Tracking"
+ buf = append(buf, ""...)
+ for i := range ab.TrackingEvents {
+ buf = appendTracking(buf, &ab.TrackingEvents[i])
+ }
+ buf = append(buf, ""...)
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendAdSource(buf []byte, as *AdSource) []byte {
+ buf = append(buf, ""...)
+ if as.VASTData != nil {
+ buf = append(buf, ""...)
+ if as.VASTData.VAST != nil {
+ buf = appendVAST(buf, as.VASTData.VAST)
+ }
+ buf = append(buf, ""...)
+ }
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendVAST(buf []byte, v *VAST) []byte {
+ // attrs: xsi, noNamespaceSchemaLocation, version
+ buf = append(buf, `')
+
+ // chardata
+ buf = escText(buf, v.Text)
+
+ for i := range v.Ad {
+ buf = appendAd(buf, &v.Ad[i])
+ }
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendAd(buf []byte, ad *Ad) []byte {
+ buf = append(buf, `')
+
+ if ad.InLine != nil {
+ buf = appendInLine(buf, ad.InLine)
+ }
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendInLine(buf []byte, il *InLine) []byte {
+ buf = append(buf, ""...)
+
+ // field order: AdSystem, AdTitle, Impression, Creatives, Extensions, Error
+ buf = append(buf, ""...)
+ buf = escText(buf, il.AdSystem)
+ buf = append(buf, ""...)
+
+ buf = append(buf, ""...)
+ buf = escText(buf, il.AdTitle)
+ buf = append(buf, ""...)
+
+ for i := range il.Impression {
+ buf = appendImpression(buf, &il.Impression[i])
+ }
+
+ // Wrappers always emitted for nested paths
+ buf = append(buf, ""...)
+ for i := range il.Creatives {
+ buf = appendCreative(buf, &il.Creatives[i])
+ }
+ buf = append(buf, ""...)
+
+ buf = append(buf, ""...)
+ for i := range il.Extensions {
+ buf = appendExtension(buf, &il.Extensions[i])
+ }
+ buf = append(buf, ""...)
+
+ if il.Error != nil {
+ buf = append(buf, ""...)
+ buf = escText(buf, il.Error.Value)
+ buf = append(buf, ""...)
+ }
+
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendImpression(buf []byte, imp *Impression) []byte {
+ buf = append(buf, `')
+ buf = escText(buf, imp.Text)
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendCreative(buf []byte, c *Creative) []byte {
+ buf = append(buf, `')
+
+ if c.UniversalAdId != nil {
+ buf = append(buf, `')
+ buf = escText(buf, c.UniversalAdId.Id)
+ buf = append(buf, ""...)
+ }
+
+ if c.Linear != nil {
+ buf = appendLinear(buf, c.Linear)
+ }
+
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendLinear(buf []byte, l *Linear) []byte {
+ buf = append(buf, ""...)
+
+ // Duration
+ buf = append(buf, ""...)
+ buf = appendDuration(buf, l.Duration)
+ buf = append(buf, ""...)
+
+ // Wrappers always emitted for nested paths
+ buf = append(buf, ""...)
+ for i := range l.TrackingEvents {
+ buf = appendTracking(buf, &l.TrackingEvents[i])
+ }
+ buf = append(buf, ""...)
+
+ buf = append(buf, ""...)
+ for i := range l.MediaFiles {
+ buf = appendMediaFile(buf, &l.MediaFiles[i])
+ }
+ buf = append(buf, ""...)
+
+ // VideoClicks (shared wrapper for ClickThrough, ClickTracking, CustomClick)
+ buf = append(buf, ""...)
+ if l.ClickThrough != nil {
+ buf = append(buf, `')
+ buf = escText(buf, l.ClickThrough.Text)
+ buf = append(buf, ""...)
+ }
+ for i := range l.ClickTracking {
+ buf = append(buf, `')
+ buf = escText(buf, l.ClickTracking[i].Text)
+ buf = append(buf, ""...)
+ }
+ for i := range l.CustomClick {
+ buf = append(buf, `')
+ buf = escText(buf, l.CustomClick[i].Text)
+ buf = append(buf, ""...)
+ }
+ buf = append(buf, ""...)
+
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendTracking(buf []byte, t *TrackingEvent) []byte {
+ buf = append(buf, `')
+ buf = escText(buf, t.Text)
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendMediaFile(buf []byte, m *MediaFile) []byte {
+ // attr order: bitrate, width, height, delivery, type, codec
+ buf = append(buf, `')
+ buf = escText(buf, m.Text)
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendExtension(buf []byte, ext *Extension) []byte {
+ buf = append(buf, `')
+
+ buf = append(buf, ""...)
+ for i := range ext.CreativeParameters {
+ buf = appendCreativeParameter(buf, &ext.CreativeParameters[i])
+ }
+ buf = append(buf, ""...)
+
+ buf = append(buf, ""...)
+ return buf
+}
+
+func appendCreativeParameter(buf []byte, cp *CreativeParameter) []byte {
+ // attr order: creativeId, name, type (Value is chardata)
+ buf = append(buf, `')
+ buf = escText(buf, cp.Value)
+ buf = append(buf, ""...)
+ return buf
+}
diff --git a/vmap/structure_test.go b/vmap/structure_test.go
index 88f8167..697c849 100644
--- a/vmap/structure_test.go
+++ b/vmap/structure_test.go
@@ -325,6 +325,196 @@ func TestSpecialCharacters(t *testing.T) {
is.Equal(vastDecoded.Ad[0].InLine.AdTitle, "Hej&ö\n<>\"")
}
+// --- Fast Marshal Tests ---
+
+func TestMarshalVmapFast(t *testing.T) {
+ is := is.New(t)
+ doc, err := os.ReadFile("sample-vmap/testVmap.xml")
+ is.NoErr(err)
+
+ var v VMAP
+ err = xml.Unmarshal(doc, &v)
+ is.NoErr(err)
+
+ expected, err := xml.Marshal(v)
+ is.NoErr(err)
+
+ got, err := MarshalVmap(&v)
+ is.NoErr(err)
+
+ is.Equal(string(expected), string(got))
+}
+
+func TestMarshalVmapEmptyFast(t *testing.T) {
+ is := is.New(t)
+ doc, err := os.ReadFile("sample-vmap/testVmap2.xml")
+ is.NoErr(err)
+
+ var v VMAP
+ err = xml.Unmarshal(doc, &v)
+ is.NoErr(err)
+
+ expected, err := xml.Marshal(v)
+ is.NoErr(err)
+
+ got, err := MarshalVmap(&v)
+ is.NoErr(err)
+
+ is.Equal(string(expected), string(got))
+}
+
+func TestMarshalVmapEmptyVastFast(t *testing.T) {
+ is := is.New(t)
+ doc, err := os.ReadFile("sample-vmap/testVmapEmptyVast.xml")
+ is.NoErr(err)
+
+ var v VMAP
+ err = xml.Unmarshal(doc, &v)
+ is.NoErr(err)
+
+ expected, err := xml.Marshal(v)
+ is.NoErr(err)
+
+ got, err := MarshalVmap(&v)
+ is.NoErr(err)
+
+ is.Equal(string(expected), string(got))
+}
+
+func TestMarshalVastFast(t *testing.T) {
+ is := is.New(t)
+ doc, err := os.ReadFile("sample-vmap/testVast.xml")
+ is.NoErr(err)
+
+ var v VAST
+ err = xml.Unmarshal(doc, &v)
+ is.NoErr(err)
+
+ expected, err := xml.Marshal(v)
+ is.NoErr(err)
+
+ got, err := MarshalVast(&v)
+ is.NoErr(err)
+
+ is.Equal(string(expected), string(got))
+}
+
+func TestMarshalVastEmptyFast(t *testing.T) {
+ is := is.New(t)
+ doc, err := os.ReadFile("sample-vmap/testVast3.xml")
+ is.NoErr(err)
+
+ var v VAST
+ err = xml.Unmarshal(doc, &v)
+ is.NoErr(err)
+
+ expected, err := xml.Marshal(v)
+ is.NoErr(err)
+
+ got, err := MarshalVast(&v)
+ is.NoErr(err)
+
+ is.Equal(string(expected), string(got))
+}
+
+func TestMarshalVast2Fast(t *testing.T) {
+ is := is.New(t)
+ doc, err := os.ReadFile("sample-vmap/testVast2.xml")
+ is.NoErr(err)
+
+ var v VAST
+ err = xml.Unmarshal(doc, &v)
+ is.NoErr(err)
+
+ expected, err := xml.Marshal(v)
+ is.NoErr(err)
+
+ got, err := MarshalVast(&v)
+ is.NoErr(err)
+
+ is.Equal(string(expected), string(got))
+}
+
+func TestMarshalSpecialCharsFast(t *testing.T) {
+ is := is.New(t)
+ doc, err := os.ReadFile("sample-vmap/testVastSpecialChars.xml")
+ is.NoErr(err)
+
+ var v VAST
+ err = xml.Unmarshal(doc, &v)
+ is.NoErr(err)
+
+ expected, err := xml.Marshal(v)
+ is.NoErr(err)
+
+ got, err := MarshalVast(&v)
+ is.NoErr(err)
+
+ is.Equal(string(expected), string(got))
+}
+
+// --- Fast Marshal Benchmarks ---
+
+func BenchmarkXMLMarshalVmap(b *testing.B) {
+ doc, err := os.ReadFile("sample-vmap/testVmap.xml")
+ if err != nil {
+ b.Fatal(err)
+ }
+ var v VMAP
+ if err := xml.Unmarshal(doc, &v); err != nil {
+ b.Fatal(err)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, _ = xml.Marshal(v)
+ }
+}
+
+func BenchmarkFastMarshalVmap(b *testing.B) {
+ doc, err := os.ReadFile("sample-vmap/testVmap.xml")
+ if err != nil {
+ b.Fatal(err)
+ }
+ var v VMAP
+ if err := xml.Unmarshal(doc, &v); err != nil {
+ b.Fatal(err)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, _ = MarshalVmap(&v)
+ }
+}
+
+func BenchmarkXMLMarshalVast(b *testing.B) {
+ doc, err := os.ReadFile("sample-vmap/testVast.xml")
+ if err != nil {
+ b.Fatal(err)
+ }
+ var v VAST
+ if err := xml.Unmarshal(doc, &v); err != nil {
+ b.Fatal(err)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, _ = xml.Marshal(v)
+ }
+}
+
+func BenchmarkFastMarshalVast(b *testing.B) {
+ doc, err := os.ReadFile("sample-vmap/testVast.xml")
+ if err != nil {
+ b.Fatal(err)
+ }
+ var v VAST
+ if err := xml.Unmarshal(doc, &v); err != nil {
+ b.Fatal(err)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, _ = MarshalVast(&v)
+ }
+}
+
func TestDecodeCompliance(t *testing.T) {
wg := sync.WaitGroup{}
//Check for race conditions