Files
Bridge-and-Join-s/internal/nsdxml/codec.go
T
fontvielle 1d6ab86a57 feat(m2m): доменная модель сообщений + парсер windows-1251 + round-trip тесты
- internal/m2m/types.go: enum'ы и simple-типы из XSD НРД (M2MSchemas_260408)
- internal/m2m/validators.go: pattern-валидаторы ReferenceID/ISIN/INN/UUID/SecurityCode/IdentityDocSerial/AccountID + перечисления
- internal/m2m/messages.go: структуры 6 типов сообщений M2M, choice-типы через указатели, IsM2M=true автоматически в MarshalXML
- internal/nsdxml/datetime.go: тип NSDDateTime (формат "YYYY-MM-DDThh:mm:ss(МСК+N)")
- internal/nsdxml/codec.go: Marshal/Unmarshal XML в windows-1251 (собственный кодек CP1251, без внешних зависимостей)
- internal/m2m/messages_test.go: round-trip тесты на 6 примерах + 2 эталонах из DOC/

Покрытие: m2m 73.9%, nsdxml 92.5%. make ci зелёный.

Отклонение от спеки: вместо golang.org/x/text/encoding/charmap собственная
таблица CP1251 на ~60 строк, потому что прокси zetit блокирует
proxy.golang.org, goproxy.cn и redirect-хосты Go-модулей.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:30:46 +03:00

132 lines
5.1 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"
"unicode/utf8"
)
// xmlProlog — пролог windows-1251 XML, который мы пишем при сериализации.
const xmlProlog = `<?xml version="1.0" encoding="windows-1251"?>` + "\n"
// cp1251High — таблица соответствия байтов 0x80..0xFF из windows-1251
// в Unicode rune. Индекс таблицы — (byte - 0x80).
//
// Источник: стандартное соответствие CP1251 (см. WHATWG encoding spec
// и MS code page 1251). Байт 0x98 в CP1251 не определён, помечаем
// U+FFFD; в эталонных XML НРД он не встречается.
var cp1251High = [128]rune{
0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021,
0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F,
0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
0xFFFD, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F,
0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7,
0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407,
0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7,
0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457,
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F,
0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F,
0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F,
}
// cp1251Low — обратная таблица rune -> byte для диапазона 0x80..0xFF.
// Строится один раз при инициализации пакета.
var cp1251Low = func() map[rune]byte {
m := make(map[rune]byte, 128)
for i, r := range cp1251High {
if r != 0xFFFD {
m[r] = byte(i) + 0x80
}
}
return m
}()
// DecodeWindows1251 преобразует байты в кодировке windows-1251 в UTF-8.
func DecodeWindows1251(src []byte) []byte {
out := make([]byte, 0, len(src))
var buf [utf8.UTFMax]byte
for _, b := range src {
var r rune
if b < 0x80 {
r = rune(b)
} else {
r = cp1251High[b-0x80]
}
n := utf8.EncodeRune(buf[:], r)
out = append(out, buf[:n]...)
}
return out
}
// ErrUnmappable возвращается, когда руна не имеет представления в windows-1251.
var ErrUnmappable = errors.New("nsdxml: rune не представим в windows-1251")
// EncodeWindows1251 преобразует UTF-8 байты в windows-1251. Если в
// исходной строке встречается руна, не выразимая в CP1251, возвращает
// ErrUnmappable с указанием руны и смещения.
func EncodeWindows1251(src []byte) ([]byte, error) {
out := make([]byte, 0, len(src))
for i := 0; i < len(src); {
r, size := utf8.DecodeRune(src[i:])
if r == utf8.RuneError && size == 1 {
return nil, fmt.Errorf("%w: некорректная UTF-8 последовательность на смещении %d", ErrUnmappable, i)
}
if r < 0x80 {
out = append(out, byte(r))
} else if b, ok := cp1251Low[r]; ok {
out = append(out, b)
} else {
return nil, fmt.Errorf("%w: U+%04X на смещении %d", ErrUnmappable, r, i)
}
i += size
}
return out, nil
}
// CharsetReader пригоден к использованию в xml.Decoder.CharsetReader.
// Для charset "windows-1251" (регистронезависимо) возвращает io.Reader,
// который читает входной поток и отдаёт его в UTF-8. Для UTF-8 и
// неуказанного charset — пробрасывает входной поток без изменений.
func CharsetReader(charset string, input io.Reader) (io.Reader, error) {
switch strings.ToLower(charset) {
case "windows-1251", "cp1251":
data, err := io.ReadAll(input)
if err != nil {
return nil, err
}
return bytes.NewReader(DecodeWindows1251(data)), nil
case "", "utf-8", "utf8":
return input, nil
}
return nil, fmt.Errorf("nsdxml: неизвестная кодировка %q", charset)
}
// Marshal сериализует v в XML, добавляет пролог с encoding="windows-1251"
// и преобразует поток в windows-1251.
func Marshal(v any) ([]byte, error) {
utf8Body, err := xml.MarshalIndent(v, "", "\t")
if err != nil {
return nil, err
}
withProlog := append([]byte(xmlProlog), utf8Body...)
return EncodeWindows1251(withProlog)
}
// 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)
}