package nsdxml import ( "bytes" "encoding/xml" "errors" "fmt" "io" "strings" "golang.org/x/text/encoding/charmap" ) // xmlProlog — пролог windows-1251 XML, который мы пишем при сериализации. const xmlProlog = `` + "\n" // ErrUnmappable возвращается, когда руна не имеет представления в windows-1251. var ErrUnmappable = errors.New("nsdxml: rune не представим в windows-1251") // DecodeWindows1251 преобразует байты в кодировке windows-1251 в UTF-8. // Использует штатную реализацию golang.org/x/text/encoding/charmap. func DecodeWindows1251(src []byte) []byte { out, _ := charmap.Windows1251.NewDecoder().Bytes(src) return out } // EncodeWindows1251 преобразует UTF-8 байты в windows-1251. Если в // исходной строке встречается руна, не выразимая в CP1251, возвращает // ErrUnmappable с указанием смещения. func EncodeWindows1251(src []byte) ([]byte, error) { out, err := charmap.Windows1251.NewEncoder().Bytes(src) if err != nil { return nil, fmt.Errorf("%w: %w", ErrUnmappable, err) } return out, nil } // CharsetReader пригоден к использованию в xml.Decoder.CharsetReader. // Для charset "windows-1251"/"cp1251" возвращает декодирующий reader. // Для UTF-8 и неуказанного charset — пробрасывает поток без изменений. func CharsetReader(charset string, input io.Reader) (io.Reader, error) { switch strings.ToLower(charset) { case "windows-1251", "cp1251": return charmap.Windows1251.NewDecoder().Reader(input), nil case "", "utf-8", "utf8": return input, nil } return nil, fmt.Errorf("nsdxml: неизвестная кодировка %q", charset) } // nsPrefixes — пространства имён НРД и их короткие префиксы. Объявляются один // раз на корневом элементе; вложенные используют префикс (req:Header, typ:GUID). // Так требует НРД: повторное xmlns="…" в каждом теге (как делает стандартный // encoding/xml) НРД считает плохо читаемым и потенциально ломающим парсинг. // Порядок — для детерминированного объявления на корне. var nsPrefixOrder = []struct{ uri, prefix string }{ {"http://nsd.ru/schemas/m2m/request", "req"}, {"http://nsd.ru/schemas/m2m/decision", "dec"}, {"http://nsd.ru/schemas/m2m/response", "resp"}, {"http://nsd.ru/schemas/m2m/handbook", "hb"}, {"http://nsd.ru/schemas/m2m/handbook/request", "hbreq"}, {"http://nsd.ru/schemas/m2m/participant/form", "pf"}, {"http://nsd.ru/schemas/m2m/types", "typ"}, } const xsiURI = "http://www.w3.org/2001/XMLSchema-instance" func nsPrefixFor(uri string) string { for _, p := range nsPrefixOrder { if p.uri == uri { return p.prefix } } return "" } // rootSchemaLocation — значение xsi:schemaLocation для корневого элемента // исходящих сообщений (как в эталонных пакетах НРД). Пусто — не добавляем. func rootSchemaLocation(rootSpace string) string { switch rootSpace { case "http://nsd.ru/schemas/m2m/request": return "http://nsd.ru/schemas/m2m/request M2MTransferRequest.xsd" case "http://nsd.ru/schemas/m2m/handbook/request": return "http://nsd.ru/schemas/m2m/handbook/request M2MTransferHandbookRequest.xsd" case "http://nsd.ru/schemas/m2m/participant/form": return "http://nsd.ru/schemas/m2m/participant/form M2MTransferParticipantForm.xsd" } return "" } // Marshal сериализует v в XML, добавляет пролог с encoding="windows-1251" // и преобразует поток в windows-1251. Пространства имён выносятся в префиксы // на корне (prefixize). func Marshal(v any) ([]byte, error) { utf8Body, err := xml.MarshalIndent(v, "", "\t") if err != nil { return nil, err } prefixed, err := prefixize(utf8Body) if err != nil { return nil, err } withProlog := append([]byte(xmlProlog), prefixed...) return EncodeWindows1251(withProlog) } // prefixize переписывает XML, сгенерированный стандартным encoding/xml // (с xmlns="…" в каждом элементе), в форму с префиксами, объявленными один раз // на корне: … // … … . Работает на уровне xml-токенов // (не строк) — надёжно к структуре. Сохраняет отступы из MarshalIndent. func prefixize(body []byte) ([]byte, error) { // Проход 1: какие namespace реально встречаются (для объявления на корне). used := map[string]bool{} xsiUsed := false { dec := xml.NewDecoder(bytes.NewReader(body)) for { tok, err := dec.Token() if err == io.EOF { break } if err != nil { return nil, err } if se, ok := tok.(xml.StartElement); ok { if se.Name.Space != "" { used[se.Name.Space] = true } for _, a := range se.Attr { if a.Name.Space == xsiURI { xsiUsed = true } } } } } dec := xml.NewDecoder(bytes.NewReader(body)) var out bytes.Buffer depth := 0 for { tok, err := dec.Token() if err == io.EOF { break } if err != nil { return nil, err } switch t := tok.(type) { case xml.StartElement: name := qname(t.Name) out.WriteByte('<') out.WriteString(name) if depth == 0 { // Объявляем префиксы используемых namespace один раз на корне — // каждое объявление на отдельной строке с отступом (как в // эталонных пакетах НРД, для читаемости). const nsIndent = "\n " for _, p := range nsPrefixOrder { if used[p.uri] { out.WriteString(nsIndent + `xmlns:` + p.prefix + `="` + p.uri + `"`) } } // xsi объявляем, если он используется в атрибутах ИЛИ если для // корня есть schemaLocation (как в эталонных пакетах НРД). schemaLoc := rootSchemaLocation(t.Name.Space) if xsiUsed || schemaLoc != "" { out.WriteString(nsIndent + `xmlns:xsi="` + xsiURI + `"`) } if schemaLoc != "" { out.WriteString(nsIndent + `xsi:schemaLocation="` + schemaLoc + `"`) } } // Атрибуты, кроме объявлений namespace (xmlns / xmlns:*). for _, a := range t.Attr { if a.Name.Local == "xmlns" || a.Name.Space == "xmlns" { continue } an := a.Name.Local if a.Name.Space == xsiURI { an = "xsi:" + a.Name.Local } else if pfx := nsPrefixFor(a.Name.Space); pfx != "" { an = pfx + ":" + a.Name.Local } out.WriteString(" " + an + `="`) writeXMLValue(&out, []byte(a.Value)) out.WriteByte('"') } out.WriteByte('>') depth++ case xml.EndElement: depth-- out.WriteString("') case xml.CharData: writeXMLValue(&out, t) case xml.Comment: out.WriteString("") } } return out.Bytes(), nil } // qname возвращает имя элемента с префиксом НРД (typ:GUID) либо без префикса // (корень/неизвестный namespace). func qname(n xml.Name) string { if pfx := nsPrefixFor(n.Space); pfx != "" { return pfx + ":" + n.Local } return n.Local } // writeXMLValue пишет текст элемента/атрибута, экранируя XML-спецсимволы. // Whitespace (отступы MarshalIndent) проходит как есть. func writeXMLValue(buf *bytes.Buffer, b []byte) { for _, c := range b { switch c { case '&': buf.WriteString("&") case '<': buf.WriteString("<") case '>': buf.WriteString(">") default: buf.WriteByte(c) } } } // Unmarshal разбирает XML (windows-1251 или UTF-8) в v. Использует // CharsetReader для перекодирования входа, если в прологе указана // windows-1251. func Unmarshal(data []byte, v any) error { dec := xml.NewDecoder(bytes.NewReader(data)) dec.CharsetReader = CharsetReader return dec.Decode(v) } // UnmarshalDefaultNS разбирает XML, подставляя defaultNS как пространство имён // для элементов БЕЗ префикса и без xmlns. Нужно для входящих сообщений НРД, // где корневой элемент идёт без namespace (напр. с // префиксами только на детях), а наша структура ждёт корень в своём namespace. func UnmarshalDefaultNS(data []byte, v any, defaultNS string) error { dec := xml.NewDecoder(bytes.NewReader(data)) dec.CharsetReader = CharsetReader dec.DefaultSpace = defaultNS return dec.Decode(v) }