// pack.go — упаковщик/распаковщик ZIP-пакетов для ИШ НРД. // // Формат отправляемого пакета (раздел 2.3 инструкции): // ZIP-архив содержит: // - .xml — сам документ (M2MTransferRequest.xml) // - config.xml — настроечный файл с указанием name и package // // Пример config.xml: // // doc.xml // #M2MTR // // // Формат входящего пакета (раздел 2.4 + раздел 2.6): // ZIP-архив содержит: // - .xml — сам документ (M2MTransferDecision.xml или M2MTransferResponse.xml) // - winf.xml — транзитный конверт ЭДО НРД // - .xml.sgn — отсоединённая подпись НРД (опц.) // // ИШ сам формирует пакет ЭДО (подписывает, добавляет winf.xml и т.д.). // Наша задача — собрать ZIP с XML+config.xml и отправить. package igw import ( "archive/zip" "bytes" "encoding/xml" "errors" "fmt" "io" "path/filepath" "strings" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml" ) // config — содержимое config.xml внутри пакета. type config struct { XMLName xml.Name `xml:"config"` Name string `xml:"name"` Package string `xml:"package"` } // PackRequest упаковывает M2MTransferRequest в ZIP-архив для ИШ. // docFileName — имя XML внутри архива (например "M2MTransferRequest.xml"). // Возвращает байты ZIP, готовые к отправке через POST /api/package/{channel}/file. func PackRequest(req *m2m.M2MTransferRequest, docFileName string) ([]byte, error) { if req == nil { return nil, errors.New("igw: PackRequest: req=nil") } if docFileName == "" { docFileName = "M2MTransferRequest.xml" } xmlBytes, err := nsdxml.Marshal(req) if err != nil { return nil, fmt.Errorf("igw: marshal M2MTransferRequest: %w", err) } return packZIP(xmlBytes, docFileName, "#M2MTR") } // PackXML упаковывает произвольный XML в ZIP с config.xml. Тип пакета // (например "#M2MTR" / "#M2MTD") задаётся явно. Полезно когда XML уже // собран снаружи (тесты, эталонные сообщения из инструкции). func PackXML(xmlBytes []byte, docFileName, packageType string) ([]byte, error) { if len(xmlBytes) == 0 { return nil, errors.New("igw: PackXML: xmlBytes пустой") } if !strings.HasPrefix(packageType, "#") { return nil, fmt.Errorf("igw: packageType должен начинаться с #, получено %q", packageType) } if docFileName == "" { docFileName = "doc.xml" } return packZIP(xmlBytes, docFileName, packageType) } func packZIP(xmlBytes []byte, docFileName, packageType string) ([]byte, error) { var buf bytes.Buffer w := zip.NewWriter(&buf) // 1. сам документ fw, err := w.Create(docFileName) if err != nil { return nil, fmt.Errorf("igw: create %s in zip: %w", docFileName, err) } if _, err := fw.Write(xmlBytes); err != nil { return nil, err } // 2. config.xml cfg := config{Name: docFileName, Package: packageType} cfgBytes, err := xml.MarshalIndent(cfg, "", " ") if err != nil { return nil, fmt.Errorf("igw: marshal config.xml: %w", err) } cfgWriter, err := w.Create("config.xml") if err != nil { return nil, fmt.Errorf("igw: create config.xml in zip: %w", err) } if _, err := cfgWriter.Write(cfgBytes); err != nil { return nil, err } if err := w.Close(); err != nil { return nil, fmt.Errorf("igw: close zip: %w", err) } return buf.Bytes(), nil } // UnpackedPackage — содержимое распакованного ZIP'а с входящим пакетом. type UnpackedPackage struct { DocXML []byte // первый XML, который не winf.xml и не config.xml WinfXML []byte // транзитный конверт ЭДО (опц., присутствует у входящих от НРД) Signature []byte // .sgn файл (отсоединённая подпись), опц. Filenames []string } // UnpackPackage распаковывает ZIP-архив от ИШ и возвращает структурированно // тело документа + winf.xml + отсоединённую подпись. func UnpackPackage(zipBytes []byte) (*UnpackedPackage, error) { r, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) if err != nil { return nil, fmt.Errorf("igw: zip reader: %w", err) } out := &UnpackedPackage{} for _, f := range r.File { out.Filenames = append(out.Filenames, f.Name) rc, err := f.Open() if err != nil { return nil, fmt.Errorf("igw: open %s in zip: %w", f.Name, err) } data, err := io.ReadAll(rc) rc.Close() if err != nil { return nil, fmt.Errorf("igw: read %s from zip: %w", f.Name, err) } low := strings.ToLower(filepath.Base(f.Name)) switch { case low == "winf.xml": out.WinfXML = data case low == "config.xml": // config.xml в исходящих, во входящих обычно отсутствует — игнорируем case strings.HasSuffix(low, ".sgn"): out.Signature = data case strings.HasSuffix(low, ".xml") && out.DocXML == nil: out.DocXML = data } } if out.DocXML == nil { return nil, fmt.Errorf("igw: в ZIP нет основного .xml документа (файлы: %v)", out.Filenames) } return out, nil } // ParseDecision разбирает DocXML входящего пакета M2MTD в m2m.M2MTransferDecision. func ParseDecision(docXML []byte) (*m2m.M2MTransferDecision, error) { var d m2m.M2MTransferDecision if err := nsdxml.Unmarshal(docXML, &d); err != nil { return nil, fmt.Errorf("igw: parse M2MTransferDecision: %w", err) } return &d, nil } // ParseResponse разбирает DocXML входящего пакета M2MER в m2m.M2MTransferResponse. func ParseResponse(docXML []byte) (*m2m.M2MTransferResponse, error) { var r m2m.M2MTransferResponse if err := nsdxml.Unmarshal(docXML, &r); err != nil { return nil, fmt.Errorf("igw: parse M2MTransferResponse: %w", err) } return &r, nil }