package nsdxml import ( "bytes" "encoding/xml" "errors" "fmt" "io" "strings" "unicode/utf8" ) // xmlProlog — пролог windows-1251 XML, который мы пишем при сериализации. const xmlProlog = `` + "\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) }