mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
222 lines
7.0 KiB
Go
222 lines
7.0 KiB
Go
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package ofdgo
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// encodeStructureNode 编码有界标引子树,引用由ObjectRef节点表达,不重复编码派生References字段
|
|
// 入参: node 根节点
|
|
// 返回: []byte XML片段, error 名称、属性或编码错误
|
|
func encodeStructureNode(node StructureNode) ([]byte, error) {
|
|
var output bytes.Buffer
|
|
encoder := xml.NewEncoder(&output)
|
|
count := 0
|
|
var write func(StructureNode, int) error
|
|
write = func(node StructureNode, depth int) error {
|
|
count++
|
|
if depth > 128 || count > 100000 {
|
|
return fmt.Errorf("custom tag structure limit exceeded")
|
|
}
|
|
if err := validateStructureNode(node); err != nil {
|
|
return err
|
|
}
|
|
start := xml.StartElement{Name: xml.Name{Space: node.Namespace, Local: node.Name}}
|
|
switch node.Namespace {
|
|
case "":
|
|
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: ""})
|
|
case "http://www.w3.org/XML/1998/namespace":
|
|
start.Name = xml.Name{Local: "xml:" + node.Name}
|
|
}
|
|
for _, attr := range node.Attributes {
|
|
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Space: attr.Namespace, Local: attr.Name}, Value: attr.Value})
|
|
}
|
|
if err := encoder.EncodeToken(start); err != nil {
|
|
return err
|
|
}
|
|
if err := encoder.EncodeToken(xml.CharData(node.Text)); err != nil {
|
|
return err
|
|
}
|
|
for _, child := range node.Children {
|
|
if err := write(child, depth+1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return encoder.EncodeToken(start.End())
|
|
}
|
|
if err := write(node, 1); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := encoder.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
return output.Bytes(), nil
|
|
}
|
|
|
|
// validateStructureNode 校验XML名称和文本,禁止重复属性及伪造命名空间声明
|
|
// 入参: node 节点字段
|
|
// 返回: error 字段校验错误
|
|
func validateStructureNode(node StructureNode) error {
|
|
if !ofdXMLIDValid(node.Name) || node.Name == "xmlns" {
|
|
return fmt.Errorf("invalid custom tag node name: %s", node.Name)
|
|
}
|
|
if !structureXMLText(node.Text) || !structureXMLText(node.Namespace) || node.Namespace == "http://www.w3.org/2000/xmlns/" {
|
|
return fmt.Errorf("invalid custom tag text or namespace")
|
|
}
|
|
seen := make(map[xml.Name]bool)
|
|
for _, attr := range node.Attributes {
|
|
name := xml.Name{Space: attr.Namespace, Local: attr.Name}
|
|
if !ofdXMLIDValid(attr.Name) || attr.Name == "xmlns" || attr.Namespace == "http://www.w3.org/2000/xmlns/" || !structureXMLText(attr.Namespace) || !structureXMLText(attr.Value) || seen[name] {
|
|
return fmt.Errorf("invalid or duplicate custom tag attribute: %s", attr.Name)
|
|
}
|
|
seen[name] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// structureXMLText 检查XML 1.0允许的字符,不以替换字符掩盖无效输入
|
|
// 入参: value 文本
|
|
// 返回: bool 是否可无损编码
|
|
func structureXMLText(value string) bool {
|
|
if !utf8.ValidString(value) {
|
|
return false
|
|
}
|
|
for _, r := range value {
|
|
if r != '\t' && r != '\n' && r != '\r' && (r < 0x20 || r == 0xfffe || r == 0xffff) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// updateStructureNodeXML 替换节点字段,保留子树继承的命名空间和非文本XML内容
|
|
// 入参: data 原XML, source 原节点, node 新字段
|
|
// 返回: []byte 独立节点片段, error 校验或编码错误
|
|
func updateStructureNodeXML(data []byte, source *editorXML, node StructureNode) ([]byte, error) {
|
|
if err := validateStructureNode(node); err != nil {
|
|
return nil, err
|
|
}
|
|
fragment, err := editorXMLStandalone(data[source.start:source.end], source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
before, err := parseStructure(context.Background(), fragment, CustomTag{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if node.Name == before.Name && node.Namespace == before.Namespace && node.Text == before.Text && slices.Equal(node.Attributes, before.Attributes) {
|
|
return data[source.start:source.end], nil
|
|
}
|
|
root, err := parseEditorXML(fragment)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if before.Text != node.Text {
|
|
var text bytes.Buffer
|
|
if err := xml.EscapeText(&text, []byte(node.Text)); err != nil {
|
|
return nil, err
|
|
}
|
|
fragment, err = editorXMLDirectText(fragment, root, text.Bytes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
root, err = parseEditorXML(fragment)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var attrs []xml.Attr
|
|
used := make(map[string]bool)
|
|
for _, attr := range root.attrs {
|
|
if attr.Name.Space == "xmlns" {
|
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + attr.Name.Local}, Value: attr.Value})
|
|
used[attr.Name.Local] = true
|
|
} else if attr.Name.Space == "" && attr.Name.Local == "xmlns" {
|
|
attrs = append(attrs, attr)
|
|
}
|
|
}
|
|
qualify := func(name, namespace string) string {
|
|
if namespace == "" {
|
|
return name
|
|
}
|
|
if namespace == "http://www.w3.org/XML/1998/namespace" {
|
|
return "xml:" + name
|
|
}
|
|
for _, attr := range attrs {
|
|
if strings.HasPrefix(attr.Name.Local, "xmlns:") && attr.Value == namespace {
|
|
return strings.TrimPrefix(attr.Name.Local, "xmlns:") + ":" + name
|
|
}
|
|
}
|
|
prefix := "tag"
|
|
for i := 1; used[prefix]; i++ {
|
|
prefix = "tag" + strconv.Itoa(i)
|
|
}
|
|
used[prefix] = true
|
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + prefix}, Value: namespace})
|
|
return prefix + ":" + name
|
|
}
|
|
name := qualify(node.Name, node.Namespace)
|
|
if node.Namespace == "" {
|
|
for i := range attrs {
|
|
if attrs[i].Name.Local == "xmlns" {
|
|
attrs[i].Value = ""
|
|
}
|
|
}
|
|
}
|
|
for _, attr := range node.Attributes {
|
|
key := qualify(attr.Name, attr.Namespace)
|
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: key}, Value: attr.Value})
|
|
}
|
|
var output bytes.Buffer
|
|
encoder := xml.NewEncoder(&output)
|
|
start := xml.StartElement{Name: xml.Name{Local: name}, Attr: attrs}
|
|
if err := encoder.EncodeToken(start); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := encoder.Flush(); err != nil {
|
|
return nil, err
|
|
}
|
|
if root.open != root.end {
|
|
if node.Namespace == "" && before.Namespace != "" {
|
|
var patches []editorXMLPatch
|
|
for _, child := range root.children {
|
|
standalone, err := editorXMLStandalone(fragment[child.start:child.end], child)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
patches = append(patches, editorXMLPatch{child.start - root.open, child.end - root.open, standalone})
|
|
}
|
|
output.Write(editorPatchXML(fragment[root.open:root.close], patches))
|
|
} else {
|
|
output.Write(fragment[root.open:root.close])
|
|
}
|
|
}
|
|
if err := encoder.EncodeToken(start.End()); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := encoder.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
return output.Bytes(), nil
|
|
}
|