Files
Bridge-and-Join-s/internal/nsdxml/codec.go
T
zuevav da12a9ec1c feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk)
- трансферные таблицы FTV_M2M: создание при установке, события, справочник участников
- сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO
- боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК
- онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML)
- модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:41:12 +03:00

253 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package nsdxml
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"strings"
"golang.org/x/text/encoding/charmap"
)
// xmlProlog — пролог windows-1251 XML, который мы пишем при сериализации.
const xmlProlog = `<?xml version="1.0" encoding="windows-1251"?>` + "\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="…" в каждом элементе), в форму с префиксами, объявленными один раз
// на корне: <M2MTransferRequest xmlns:req="…" xmlns:typ="…"> … <req:Header>
// <typ:GUID>…</typ:GUID> … </req:Header>. Работает на уровне 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("</")
out.WriteString(qname(t.Name))
out.WriteByte('>')
case xml.CharData:
writeXMLValue(&out, t)
case xml.Comment:
out.WriteString("<!--")
out.Write(t)
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("&amp;")
case '<':
buf.WriteString("&lt;")
case '>':
buf.WriteString("&gt;")
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 (напр. <M2MTransferRequest> с
// префиксами только на детях), а наша структура ждёт корень в своём 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)
}