da12a9ec1c
- 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>
253 lines
9.3 KiB
Go
253 lines
9.3 KiB
Go
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("&")
|
||
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 (напр. <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)
|
||
}
|