服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
549 lines
19 KiB
Go
549 lines
19 KiB
Go
package annotations
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
appconfig "pvfine/config"
|
|
)
|
|
|
|
var (
|
|
defaultEngineOnce sync.Once
|
|
defaultEngine *Engine
|
|
defaultEngineErr error
|
|
)
|
|
|
|
// LoadDefault parses the embedded rule set once and reuses the immutable
|
|
// engine. The generated path notes make the document large enough that a parse
|
|
// per preview or auxiliary core would be measurable.
|
|
func LoadDefault() (*Engine, error) {
|
|
defaultEngineOnce.Do(func() {
|
|
defaultEngine, defaultEngineErr = loadDefault()
|
|
})
|
|
return defaultEngine, defaultEngineErr
|
|
}
|
|
|
|
// generatedRuleFile is one bulk rule set that ships next to annotations.json.
|
|
type generatedRuleFile struct {
|
|
// name is the sibling file that overrides the embedded copy at runtime.
|
|
name string
|
|
label string
|
|
data []byte
|
|
}
|
|
|
|
// generatedRuleFiles lists the generated rule sets merged after the hand-written
|
|
// rules. Order only decides the primary title of a path or section, so the
|
|
// curated rules in annotations.json always stay first.
|
|
func generatedRuleFiles() []generatedRuleFile {
|
|
return []generatedRuleFile{
|
|
{name: "paths.json", label: "path 标注", data: appconfig.PathsJSON},
|
|
{name: "sections.json", label: "section 标注", data: appconfig.SectionsJSON},
|
|
}
|
|
}
|
|
|
|
func loadDefault() (*Engine, error) {
|
|
document, err := parseDocument(appconfig.AnnotationsJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, generated := range generatedRuleFiles() {
|
|
if err := mergeGeneratedRules(&document, generated.data, generated.label); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
lists, err := ParseLists(appconfig.ListsJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
document.Relations = lists.Relations
|
|
if err := Validate(document); err != nil {
|
|
return nil, err
|
|
}
|
|
return Compile(document)
|
|
}
|
|
|
|
// mergeGeneratedRules appends a generated rule set after the hand-written rules,
|
|
// so an explicit rule keeps the primary title for a path or section. Rules whose
|
|
// ID is already present are skipped, which keeps merging idempotent when a user
|
|
// mirrors a generated file by hand.
|
|
func mergeGeneratedRules(document *Document, data []byte, label string) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
generated, err := parseDocument(data)
|
|
if err != nil {
|
|
return fmt.Errorf("解析 %s 失败: %w", label, err)
|
|
}
|
|
known := make(map[string]struct{}, len(document.Rules))
|
|
for _, rule := range document.Rules {
|
|
known[rule.ID] = struct{}{}
|
|
}
|
|
for _, rule := range generated.Rules {
|
|
if _, exists := known[rule.ID]; exists {
|
|
continue
|
|
}
|
|
known[rule.ID] = struct{}{}
|
|
document.Rules = append(document.Rules, rule)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Parse(data []byte) (Document, error) {
|
|
document, err := parseDocument(data)
|
|
if err != nil {
|
|
return Document{}, err
|
|
}
|
|
if err := Validate(document); err != nil {
|
|
return Document{}, err
|
|
}
|
|
return document, nil
|
|
}
|
|
|
|
func ParseRules(data []byte) (Document, error) {
|
|
return parseDocument(data)
|
|
}
|
|
|
|
func parseDocument(data []byte) (Document, error) {
|
|
var document Document
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&document); err != nil {
|
|
return Document{}, fmt.Errorf("解析标注规则失败: %w", err)
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return Document{}, fmt.Errorf("标注规则只能包含一个 JSON 文档")
|
|
}
|
|
return Document{}, fmt.Errorf("解析标注规则失败: %w", err)
|
|
}
|
|
normalizeImageTargets(&document)
|
|
return document, nil
|
|
}
|
|
|
|
func ParseLists(data []byte) (ListDocument, error) {
|
|
var lists ListDocument
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&lists); err != nil {
|
|
return ListDocument{}, fmt.Errorf("解析列表配置失败: %w", err)
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return ListDocument{}, fmt.Errorf("列表配置只能包含一个 JSON 文档")
|
|
}
|
|
return ListDocument{}, fmt.Errorf("解析列表配置失败: %w", err)
|
|
}
|
|
document := Document{Version: lists.Version, Relations: lists.Relations}
|
|
if err := Validate(document); err != nil {
|
|
return ListDocument{}, err
|
|
}
|
|
return lists, nil
|
|
}
|
|
|
|
func Marshal(document Document) ([]byte, error) {
|
|
normalizeImageTargets(&document)
|
|
if err := Validate(document); err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := json.MarshalIndent(document, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(data, '\n'), nil
|
|
}
|
|
|
|
func MarshalRules(document Document) ([]byte, error) {
|
|
normalizeImageTargets(&document)
|
|
if err := Validate(document); err != nil {
|
|
return nil, err
|
|
}
|
|
document.Relations = nil
|
|
data, err := json.MarshalIndent(document, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(data, '\n'), nil
|
|
}
|
|
|
|
func MarshalLists(lists ListDocument) ([]byte, error) {
|
|
document := Document{Version: lists.Version, Relations: lists.Relations}
|
|
if err := Validate(document); err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := json.MarshalIndent(lists, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(data, '\n'), nil
|
|
}
|
|
|
|
func Validate(document Document) error {
|
|
normalizeImageTargets(&document)
|
|
problems := make([]string, 0)
|
|
validateVersions := func(prefix string, versions []string) {
|
|
for _, version := range versions {
|
|
if !ValidPVFVersion(version) {
|
|
problems = append(problems, fmt.Sprintf("%s.pvfVersions 包含无效版本 %q,只允许 90US、90CN、110US、115US、115CN、100CN、TW", prefix, version))
|
|
}
|
|
}
|
|
}
|
|
for i, rule := range document.Rules {
|
|
validateVersions(fmt.Sprintf("rules[%d]", i), rule.PVFVersions)
|
|
}
|
|
for i, field := range document.Fields {
|
|
validateVersions(fmt.Sprintf("fields[%d]", i), field.PVFVersions)
|
|
}
|
|
|
|
if document.Version != 1 {
|
|
problems = append(problems, fmt.Sprintf("version 必须为 1,当前为 %d", document.Version))
|
|
}
|
|
|
|
relationNames := make([]string, 0, len(document.Relations))
|
|
for name := range document.Relations {
|
|
relationNames = append(relationNames, name)
|
|
}
|
|
sort.Strings(relationNames)
|
|
for _, name := range relationNames {
|
|
relation := document.Relations[name]
|
|
prefix := fmt.Sprintf("relations.%s", name)
|
|
if strings.TrimSpace(name) == "" {
|
|
problems = append(problems, "relation 名称不能为空")
|
|
}
|
|
kind := relation.Kind
|
|
if kind == "" {
|
|
kind = "list"
|
|
}
|
|
if kind != "list" && kind != "contextual" && kind != "union" {
|
|
problems = append(problems, prefix+".kind 必须是 list、contextual 或 union")
|
|
}
|
|
if kind == "union" {
|
|
if len(relation.Relations) == 0 {
|
|
problems = append(problems, prefix+".relations 不能为空")
|
|
}
|
|
for _, member := range relation.Relations {
|
|
if strings.TrimSpace(member) == "" || member == name {
|
|
problems = append(problems, prefix+".relations 包含无效关联类型")
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if kind == "list" && strings.TrimSpace(relation.ListPath) == "" {
|
|
problems = append(problems, prefix+".listPath 不能为空")
|
|
}
|
|
if relation.IDToken < 0 || relation.PathToken < 0 || relation.ContextToken < 0 {
|
|
problems = append(problems, prefix+" 的 token 下标不能为负数")
|
|
}
|
|
if kind == "contextual" {
|
|
if len(relation.ContextPaths) == 0 {
|
|
problems = append(problems, prefix+".contextPaths 不能为空")
|
|
}
|
|
for context, listPath := range relation.ContextPaths {
|
|
if strings.TrimSpace(context) == "" || strings.TrimSpace(listPath) == "" {
|
|
problems = append(problems, prefix+".contextPaths 的键和值不能为空")
|
|
break
|
|
}
|
|
}
|
|
if relation.RecordTokens <= relation.IDToken || relation.RecordTokens <= relation.PathToken {
|
|
problems = append(problems, prefix+".recordTokens 必须覆盖 idToken 和 pathToken")
|
|
}
|
|
}
|
|
recordTokens := relation.RecordTokens
|
|
if recordTokens == 0 {
|
|
recordTokens = max(relation.IDToken, relation.PathToken) + 1
|
|
}
|
|
if recordTokens <= relation.IDToken || recordTokens <= relation.PathToken {
|
|
problems = append(problems, prefix+".recordTokens 必须覆盖 idToken 和 pathToken")
|
|
}
|
|
if strings.TrimSpace(relation.NameSection) == "" {
|
|
problems = append(problems, prefix+".nameSection 不能为空")
|
|
}
|
|
}
|
|
unionState := make(map[string]uint8)
|
|
var visitUnion func(string)
|
|
visitUnion = func(name string) {
|
|
if unionState[name] == 2 {
|
|
return
|
|
}
|
|
if unionState[name] == 1 {
|
|
problems = append(problems, "relations."+name+" 存在循环引用")
|
|
return
|
|
}
|
|
relation, ok := document.Relations[name]
|
|
if !ok || relation.Kind != "union" {
|
|
unionState[name] = 2
|
|
return
|
|
}
|
|
unionState[name] = 1
|
|
for _, member := range relation.Relations {
|
|
if _, ok := document.Relations[member]; !ok {
|
|
problems = append(problems, fmt.Sprintf("relations.%s 引用了不存在的 relation: %s", name, member))
|
|
continue
|
|
}
|
|
visitUnion(member)
|
|
}
|
|
unionState[name] = 2
|
|
}
|
|
for _, name := range relationNames {
|
|
visitUnion(name)
|
|
}
|
|
|
|
seenIDs := make(map[string]bool)
|
|
for i, originalRule := range document.Rules {
|
|
rule := originalRule
|
|
if strings.TrimSpace(originalRule.Field) != "" {
|
|
resolved, err := resolveFieldRule(originalRule, document.Fields)
|
|
if err != nil {
|
|
problems = append(problems, fmt.Sprintf("rules[%d]: %s", i, err))
|
|
continue
|
|
}
|
|
rule = resolved
|
|
}
|
|
prefix := fmt.Sprintf("rules[%d]", i)
|
|
if strings.TrimSpace(rule.ID) == "" {
|
|
problems = append(problems, prefix+".id 不能为空")
|
|
} else if seenIDs[rule.ID] {
|
|
problems = append(problems, prefix+".id 重复: "+rule.ID)
|
|
}
|
|
seenIDs[rule.ID] = true
|
|
|
|
for _, extension := range rule.Match.Extensions {
|
|
if extension == "" || !strings.HasPrefix(extension, ".") || strings.ContainsAny(extension, "/\\") {
|
|
problems = append(problems, prefix+".match.extensions 必须是以点开头的文件后缀")
|
|
break
|
|
}
|
|
}
|
|
if rule.Match.Glob != "" {
|
|
if err := validateGlob(rule.Match.Glob); err != nil {
|
|
problems = append(problems, prefix+".match.glob 无效: "+err.Error())
|
|
}
|
|
}
|
|
|
|
switch rule.Target.Kind {
|
|
case "path":
|
|
if rule.Target.Offset != 0 || rule.Target.GroupOffset != 0 || len(rule.Target.StandaloneValues) > 0 || rule.Target.TokensPerLineIndex != nil {
|
|
problems = append(problems, prefix+" 的 offset/tokensPerLineIndex 只允许用于 token 标注")
|
|
}
|
|
if rule.Annotation.Type != "text" {
|
|
problems = append(problems, prefix+" 的 path 标注只支持 text 类型")
|
|
}
|
|
case "section":
|
|
if rule.Target.Offset != 0 || rule.Target.GroupOffset != 0 || len(rule.Target.StandaloneValues) > 0 || rule.Target.TokensPerLineIndex != nil {
|
|
problems = append(problems, prefix+" 的 offset/tokensPerLineIndex 只允许用于 token 标注")
|
|
}
|
|
if strings.TrimSpace(rule.Target.Section) == "" {
|
|
problems = append(problems, prefix+".target.section 不能为空")
|
|
}
|
|
if rule.Annotation.Type != "text" {
|
|
problems = append(problems, prefix+" 的 section 标注只支持 text 类型")
|
|
}
|
|
case "token":
|
|
if rule.Annotation.Type != "image" && rule.Target.ImagePathToken != nil {
|
|
problems = append(problems, prefix+".target.imagePathToken 只允许用于 image 标注")
|
|
}
|
|
if strings.TrimSpace(rule.Target.Section) == "" {
|
|
problems = append(problems, prefix+".target.section 不能为空")
|
|
}
|
|
if (rule.Target.Index == nil) == (rule.Target.Range == nil) {
|
|
problems = append(problems, prefix+" 的 token 目标必须且只能配置 index 或 range")
|
|
}
|
|
if rule.Target.Index != nil && *rule.Target.Index < 0 {
|
|
problems = append(problems, prefix+".target.index 不能为负数")
|
|
}
|
|
if rule.Target.Offset < 0 {
|
|
problems = append(problems, prefix+".target.offset 不能为负数")
|
|
}
|
|
if rule.Target.GroupOffset < 0 {
|
|
problems = append(problems, prefix+".target.groupOffset 不能为负数")
|
|
}
|
|
if rule.Target.TokensPerLineIndex != nil && *rule.Target.TokensPerLineIndex < 0 {
|
|
problems = append(problems, prefix+".target.tokensPerLineIndex 不能为负数")
|
|
}
|
|
if rule.Target.RecordTokens < 0 {
|
|
problems = append(problems, prefix+".target.recordTokens 不能为负数")
|
|
}
|
|
if rule.Target.RecordTokens == 0 && rule.Target.ContextIndex != nil {
|
|
problems = append(problems, prefix+".target.contextIndex 需要同时配置 recordTokens")
|
|
}
|
|
if rule.Target.RecordTokens > 0 {
|
|
if rule.Target.Index == nil {
|
|
problems = append(problems, prefix+".target.recordTokens 需要配合单个 index 使用")
|
|
} else if *rule.Target.Index >= rule.Target.RecordTokens {
|
|
problems = append(problems, prefix+".target.index 必须小于 recordTokens")
|
|
}
|
|
if rule.Target.ContextIndex != nil && (*rule.Target.ContextIndex < 0 || *rule.Target.ContextIndex >= rule.Target.RecordTokens) {
|
|
problems = append(problems, prefix+".target.contextIndex 必须位于 recordTokens 范围内")
|
|
}
|
|
}
|
|
if rule.Target.Offset != 0 || rule.Target.GroupOffset != 0 || len(rule.Target.StandaloneValues) > 0 || rule.Target.TokensPerLineIndex != nil {
|
|
if rule.Target.RecordTokens <= 0 {
|
|
problems = append(problems, prefix+".target.offset/tokensPerLineIndex 需要配置正数 recordTokens 作为回退值")
|
|
}
|
|
if rule.Target.Index == nil || rule.Target.Range != nil {
|
|
problems = append(problems, prefix+".target.offset/tokensPerLineIndex 需要配合单个 index 使用")
|
|
}
|
|
}
|
|
if rule.Target.Range != nil {
|
|
if rule.Target.Range.Start < 0 || rule.Target.Range.EndExclusive <= rule.Target.Range.Start {
|
|
problems = append(problems, prefix+".target.range 必须满足 0 <= start < endExclusive")
|
|
}
|
|
if rule.Annotation.Type != "text" {
|
|
problems = append(problems, prefix+" 的 token 范围只支持 text 类型")
|
|
}
|
|
}
|
|
if rule.Annotation.Type == "image" {
|
|
if rule.Target.Index == nil || rule.Target.Range != nil {
|
|
problems = append(problems, prefix+" 的 image 标注必须使用单个路径 token")
|
|
}
|
|
if rule.Target.RecordTokens <= 0 {
|
|
problems = append(problems, prefix+" 的 image 标注必须配置正数 target.recordTokens")
|
|
}
|
|
if rule.Target.ImagePathToken == nil {
|
|
problems = append(problems, prefix+" 的 image 标注必须配置 target.imagePathToken")
|
|
} else if rule.Target.RecordTokens > 0 && (*rule.Target.ImagePathToken < 0 || *rule.Target.ImagePathToken >= rule.Target.RecordTokens) {
|
|
problems = append(problems, prefix+".target.imagePathToken 必须位于 recordTokens 范围内")
|
|
}
|
|
if rule.Target.Index != nil && rule.Target.ImagePathToken != nil && *rule.Target.Index == *rule.Target.ImagePathToken {
|
|
problems = append(problems, prefix+" 的 imagePathToken 不能与图片索引 token 相同")
|
|
}
|
|
if rule.Target.ContextIndex != nil {
|
|
problems = append(problems, prefix+" 的 image 标注不支持 contextIndex")
|
|
}
|
|
}
|
|
default:
|
|
problems = append(problems, prefix+".target.kind 必须是 path、section 或 token")
|
|
}
|
|
|
|
switch rule.Annotation.Type {
|
|
case "text":
|
|
case "image":
|
|
case "path":
|
|
if root := strings.TrimSpace(strings.ReplaceAll(rule.Annotation.PathRoot, "\\", "/")); root != "" && (strings.HasPrefix(root, "/") || root == ".." || strings.HasPrefix(root, "../") || strings.Contains(root, "/../")) {
|
|
problems = append(problems, prefix+".annotation.pathRoot 必须是归档内的相对目录")
|
|
}
|
|
case "enum":
|
|
if len(rule.Annotation.Values) == 0 {
|
|
problems = append(problems, prefix+".annotation.values 不能为空")
|
|
}
|
|
case "reference":
|
|
relation, ok := document.Relations[rule.Annotation.Relation]
|
|
if !ok {
|
|
problems = append(problems, prefix+" 引用了不存在的 relation: "+rule.Annotation.Relation)
|
|
} else if relation.Kind == "contextual" {
|
|
if rule.Target.RecordTokens == 0 {
|
|
problems = append(problems, prefix+" 的 contextual relation 需要配置 target.recordTokens")
|
|
}
|
|
contextIndex := relation.ContextToken
|
|
if rule.Target.ContextIndex != nil {
|
|
contextIndex = *rule.Target.ContextIndex
|
|
}
|
|
if rule.Target.RecordTokens > 0 && (contextIndex < 0 || contextIndex >= rule.Target.RecordTokens) {
|
|
problems = append(problems, prefix+" 的 contextual relation 上下文 token 下标必须位于 target.recordTokens 范围内")
|
|
}
|
|
}
|
|
default:
|
|
problems = append(problems, prefix+".annotation.type 必须是 text、image、enum、reference 或 path")
|
|
}
|
|
if rule.Annotation.Type != "path" && rule.Annotation.PathRoot != "" {
|
|
problems = append(problems, prefix+".annotation.pathRoot 只允许用于 path 标注")
|
|
}
|
|
if rule.Annotation.InlineImage && rule.Annotation.Type != "image" {
|
|
problems = append(problems, prefix+".annotation.inlineImage 只允许用于 image 标注")
|
|
}
|
|
}
|
|
|
|
seenFieldIDs := make(map[string]bool, len(document.Fields))
|
|
for i, field := range document.Fields {
|
|
prefix := fmt.Sprintf("fields[%d]", i)
|
|
if strings.TrimSpace(field.ID) == "" {
|
|
problems = append(problems, prefix+".id 不能为空")
|
|
} else if seenFieldIDs[field.ID] {
|
|
problems = append(problems, prefix+".id 重复: "+field.ID)
|
|
}
|
|
seenFieldIDs[field.ID] = true
|
|
// Reuse the mature rule validator for the shared target and annotation
|
|
// schema. Fields are not rules at runtime, so this temporary document
|
|
// cannot recurse into the fields collection.
|
|
if err := Validate(Document{
|
|
Version: 1, Relations: document.Relations,
|
|
Rules: []Rule{{ID: field.ID, Match: field.Match, Target: field.Target, Annotation: field.Annotation}},
|
|
}); err != nil {
|
|
problems = append(problems, prefix+": "+err.Error())
|
|
}
|
|
if field.Preview != nil {
|
|
if len(field.Preview.providerNames()) == 0 {
|
|
problems = append(problems, prefix+".preview.provider 不能为空")
|
|
}
|
|
if strings.TrimSpace(field.Preview.Role) == "" {
|
|
problems = append(problems, prefix+".preview.role 不能为空")
|
|
}
|
|
if strings.TrimSpace(field.Preview.Group) == "" {
|
|
problems = append(problems, prefix+".preview.group 不能为空")
|
|
}
|
|
if strings.TrimSpace(field.Preview.Format) == "" {
|
|
problems = append(problems, prefix+".preview.format 不能为空")
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(problems) > 0 {
|
|
return fmt.Errorf("标注规则校验失败:\n- %s", strings.Join(problems, "\n- "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// normalizeImageTargets migrates the original image rule shape. Originally
|
|
// target.index was the IMG path token and target.imageIndexToken was the
|
|
// numeric image index token. Image annotations now anchor on the numeric
|
|
// index and use imagePathToken for the associated IMG path token.
|
|
func normalizeImageTargets(document *Document) {
|
|
if document == nil {
|
|
return
|
|
}
|
|
document.Rules = append([]Rule(nil), document.Rules...)
|
|
for i := range document.Rules {
|
|
normalizeImageTarget(&document.Rules[i].Target, document.Rules[i].Annotation.Type)
|
|
}
|
|
document.Fields = append([]FieldDefinition(nil), document.Fields...)
|
|
for i := range document.Fields {
|
|
normalizeImageTarget(&document.Fields[i].Target, document.Fields[i].Annotation.Type)
|
|
}
|
|
}
|
|
|
|
func normalizeImageTarget(target *TargetSpec, annotationType string) {
|
|
if target == nil || annotationType != "image" || target.ImageIndexToken == nil {
|
|
return
|
|
}
|
|
if target.ImagePathToken == nil {
|
|
pathToken := target.Index
|
|
target.Index = target.ImageIndexToken
|
|
target.ImagePathToken = pathToken
|
|
}
|
|
target.ImageIndexToken = nil
|
|
}
|
|
|
|
func validateGlob(pattern string) error {
|
|
for _, segment := range strings.Split(normalizePath(pattern), "/") {
|
|
if segment == "**" {
|
|
continue
|
|
}
|
|
if _, err := path.Match(segment, segment); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func max(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|