DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
// Package pvf 提供 DNF 项目层 PVF 文本解析和内存索引。
//
// 平台层 internal/platform/pvf 只负责 PVF 文件归档、解密、解压和按路径读文本。
// 本包负责把这些文本解析成 DNF 项目可消费的通用节点、section 和 lst 引用索引。
package pvf
+326
View File
@@ -0,0 +1,326 @@
package pvf
import (
"context"
"fmt"
"path"
"sort"
"strings"
platformpvf "longheng.io/server/internal/platform/pvf"
)
// Source 表示可以按路径读取 PVF 文本的内存来源。
type Source interface {
ReadText(relativePath string) (string, error)
}
// BuildOptions 控制 DNF PVF 内存索引的预加载范围。
type BuildOptions struct {
All bool
Paths []string
Lists []string
Prefixes []string
}
// Index 保存已解析的 DNF PVF 文档和 `.lst` 引用索引。
type Index struct {
docs map[string]*Document
lists map[string][]ListEntry
refs map[string]map[int64]string
paths []string
}
type fileLister interface {
Files() []platformpvf.File
}
type fileFinder interface {
FindFile(relativePath string) (platformpvf.File, bool)
}
type filePathRanger interface {
RangePaths(func(string) bool)
}
type sourcePathIndex struct {
paths map[string]string
finder fileFinder
}
func (i sourcePathIndex) find(value string) (string, bool) {
clean := cleanPath(value)
if clean == "" {
return "", false
}
if actual, ok := i.paths[pathKey(clean)]; ok {
return actual, true
}
if i.finder == nil {
return "", false
}
file, ok := i.finder.FindFile(clean)
if !ok {
return "", false
}
actual := cleanPath(file.ArchivePath)
return actual, actual != ""
}
// Build 从已加载的 PVF source 中构建项目层内存索引。
func Build(ctx context.Context, source Source, options BuildOptions) (*Index, error) {
if source == nil {
return nil, ErrSourceRequired
}
if ctx == nil {
ctx = context.Background()
}
sourcePaths := sourcePathSet(source)
paths := collectPaths(source, options, sourcePaths)
listKeys := listKeySet(options.Lists)
listPaths := append([]string(nil), options.Lists...)
index := &Index{
docs: make(map[string]*Document, len(paths)),
lists: make(map[string][]ListEntry, len(options.Lists)),
refs: make(map[string]map[int64]string, len(options.Lists)),
paths: make([]string, 0, len(paths)),
}
for pos := 0; pos < len(paths); pos++ {
docPath := paths[pos]
if err := ctx.Err(); err != nil {
return nil, err
}
text, err := source.ReadText(docPath)
if err != nil {
return nil, fmt.Errorf("read dnf pvf %q: %w", docPath, err)
}
doc, err := Parse(docPath, text)
if err != nil {
return nil, fmt.Errorf("parse dnf pvf %q: %w", docPath, err)
}
key := pathKey(docPath)
index.docs[key] = doc
index.paths = append(index.paths, doc.Path)
if _, ok := listKeys[key]; ok {
for _, entry := range ParseList(doc) {
resolved := resolveListRef(doc.Path, entry.Path, sourcePaths)
addPath(&paths, index.docs, resolved)
if strings.EqualFold(path.Ext(resolved), ".lst") {
resolvedKey := pathKey(resolved)
if _, exists := listKeys[resolvedKey]; !exists {
listKeys[resolvedKey] = struct{}{}
listPaths = append(listPaths, resolved)
}
}
}
}
}
sort.Strings(index.paths)
index.buildLists(listPaths, sourcePaths)
return index, nil
}
// Document 按路径返回已解析文档,不会重新读取 PVF。
func (i *Index) Document(docPath string) (*Document, bool) {
if i == nil {
return nil, false
}
doc, ok := i.docs[pathKey(docPath)]
return doc, ok
}
// Paths 返回当前索引中的文档路径副本。
func (i *Index) Paths() []string {
if i == nil || len(i.paths) == 0 {
return nil
}
out := make([]string, len(i.paths))
copy(out, i.paths)
return out
}
// List 返回指定 `.lst` 文档的引用列表副本。
func (i *Index) List(listPath string) []ListEntry {
if i == nil {
return nil
}
entries := i.lists[pathKey(listPath)]
out := make([]ListEntry, len(entries))
copy(out, entries)
return out
}
// Resolve 从指定 `.lst` 索引中按 id 查找 PVF 路径。
func (i *Index) Resolve(listPath string, id int64) (string, bool) {
if i == nil {
return "", false
}
refs := i.refs[pathKey(listPath)]
if refs == nil {
return "", false
}
value, ok := refs[id]
return value, ok
}
// Snapshot 返回当前内存索引的文档、列表和引用数量。
func (i *Index) Snapshot() Snapshot {
if i == nil {
return Snapshot{}
}
refs := 0
for _, entries := range i.lists {
refs += len(entries)
}
return Snapshot{
Documents: len(i.docs),
Lists: len(i.lists),
Refs: refs,
}
}
func (i *Index) buildLists(lists []string, sourcePaths sourcePathIndex) {
for _, listPath := range lists {
key := pathKey(listPath)
doc := i.docs[key]
if doc == nil {
continue
}
entries := ParseList(doc)
for idx := range entries {
entries[idx].Path = resolveListRef(doc.Path, entries[idx].Path, sourcePaths)
}
i.lists[key] = entries
refMap := make(map[int64]string, len(entries))
for _, entry := range entries {
if _, exists := refMap[entry.ID]; !exists {
refMap[entry.ID] = entry.Path
}
}
i.refs[key] = refMap
}
}
func collectPaths(source Source, options BuildOptions, sourcePaths sourcePathIndex) []string {
pathSet := make(map[string]struct{})
paths := make([]string, 0, len(options.Paths)+len(options.Lists))
addPath := func(value string) {
clean := cleanPath(value)
if clean == "" {
return
}
key := pathKey(clean)
if _, exists := pathSet[key]; exists {
return
}
pathSet[key] = struct{}{}
if actual, ok := sourcePaths.find(clean); ok {
paths = append(paths, actual)
return
}
paths = append(paths, clean)
}
for _, value := range options.Paths {
addPath(value)
}
for _, value := range options.Lists {
addPath(value)
}
if ranger, ok := source.(filePathRanger); ok && (options.All || len(options.Prefixes) > 0) {
ranger.RangePaths(func(archivePath string) bool {
if options.All || matchPrefix(archivePath, options.Prefixes) {
addPath(archivePath)
}
return true
})
} else if lister, ok := source.(fileLister); ok && (options.All || len(options.Prefixes) > 0) {
for _, file := range lister.Files() {
if options.All || matchPrefix(file.ArchivePath, options.Prefixes) {
addPath(file.ArchivePath)
}
}
}
sort.Strings(paths)
return paths
}
func sourcePathSet(source Source) sourcePathIndex {
if finder, ok := source.(fileFinder); ok {
return sourcePathIndex{finder: finder}
}
lister, ok := source.(fileLister)
if !ok {
return sourcePathIndex{}
}
files := lister.Files()
if len(files) == 0 {
return sourcePathIndex{}
}
out := make(map[string]string, len(files))
for _, file := range files {
clean := cleanPath(file.ArchivePath)
key := pathKey(clean)
if key == "" {
continue
}
if _, exists := out[key]; !exists {
out[key] = clean
}
}
return sourcePathIndex{paths: out}
}
func resolveListRef(listPath, refPath string, sourcePaths sourcePathIndex) string {
ref := cleanPath(refPath)
if ref == "" {
return ""
}
if actual, ok := sourcePaths.find(ref); ok {
return actual
}
listDir := cleanPath(path.Dir(cleanPath(listPath)))
if listDir == "." {
listDir = ""
}
if listDir != "" {
candidate := cleanPath(path.Join(listDir, ref))
if actual, ok := sourcePaths.find(candidate); ok {
return actual
}
listDirKey := pathKey(listDir)
refKey := pathKey(ref)
if listDirKey != "" && strings.HasPrefix(refKey, listDirKey+"/") {
return ref
}
return candidate
}
return ref
}
func addPath(paths *[]string, docs map[string]*Document, value string) {
clean := cleanPath(value)
if clean == "" {
return
}
key := pathKey(clean)
if _, exists := docs[key]; exists {
return
}
for _, existing := range *paths {
if pathKey(existing) == key {
return
}
}
*paths = append(*paths, clean)
}
func listKeySet(lists []string) map[string]struct{} {
out := make(map[string]struct{}, len(lists))
for _, value := range lists {
key := pathKey(value)
if key != "" {
out[key] = struct{}{}
}
}
return out
}
+29
View File
@@ -0,0 +1,29 @@
package pvf
// ParseList 从 `.lst` 文档中提取 id 到 PVF 路径的引用。
func ParseList(doc *Document) []ListEntry {
if doc == nil || len(doc.Tokens) == 0 {
return nil
}
entries := make([]ListEntry, 0)
for idx := 0; idx+1 < len(doc.Tokens); idx++ {
id := doc.Tokens[idx]
if id.Kind != TokenInt {
continue
}
ref := doc.Tokens[idx+1]
if ref.Kind != TokenString && ref.Kind != TokenIdent {
continue
}
refPath := cleanPath(ref.Value)
if refPath == "" {
continue
}
entries = append(entries, ListEntry{
ID: id.Int,
Path: refPath,
})
idx++
}
return entries
}
+223
View File
@@ -0,0 +1,223 @@
package pvf
import (
"fmt"
"strconv"
"strings"
"unicode"
)
// Parse 把单个 PVF 文本文档解析成通用 token 和 section。
func Parse(docPath, text string) (*Document, error) {
docPath = cleanPath(docPath)
if docPath == "" {
return nil, ErrPathRequired
}
parser := docParser{
doc: Document{Path: docPath},
}
lines := strings.Split(text, "\n")
for idx, line := range lines {
if err := parser.parseLine(idx+1, line); err != nil {
return nil, err
}
}
if parser.pending != nil {
return nil, fmt.Errorf("dnf pvf string at %d:%d is not closed", parser.pending.line, parser.pending.column)
}
parser.closeSection()
return &parser.doc, nil
}
type docParser struct {
doc Document
pending *pendingString
}
type pendingString struct {
quote byte
line int
column int
raw strings.Builder
value strings.Builder
}
func (p *docParser) parseLine(lineNo int, line string) error {
if p.pending != nil {
return p.continueString(lineNo, line)
}
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//") {
return nil
}
if strings.HasPrefix(trimmed, "[") {
if end := strings.IndexByte(trimmed, ']'); end > 0 {
column := strings.Index(line, "[") + 1
name := strings.TrimSpace(trimmed[1:end])
p.addToken(Token{
Kind: TokenSection,
Raw: trimmed[:end+1],
Value: name,
Line: lineNo,
Column: column,
})
return p.scanValues(lineNo, column+end+1, trimmed[end+1:])
}
}
column := len(line) - len(strings.TrimLeftFunc(line, unicode.IsSpace)) + 1
return p.scanValues(lineNo, column, trimmed)
}
func (p *docParser) addToken(token Token) {
if token.Kind == TokenSection {
p.closeSection()
p.doc.Sections = append(p.doc.Sections, Section{
Name: token.Value,
Start: len(p.doc.Tokens) + 1,
})
}
p.doc.Tokens = append(p.doc.Tokens, token)
}
func (p *docParser) closeSection() {
if len(p.doc.Sections) == 0 {
return
}
idx := len(p.doc.Sections) - 1
if p.doc.Sections[idx].End == 0 {
p.doc.Sections[idx].End = len(p.doc.Tokens)
}
}
func (p *docParser) scanValues(lineNo, baseColumn int, line string) error {
for pos := 0; pos < len(line); {
if isSpace(line[pos]) {
pos++
continue
}
if pos+1 < len(line) && line[pos] == '/' && line[pos+1] == '/' {
return nil
}
if line[pos] == '#' {
return nil
}
column := baseColumn + pos
switch line[pos] {
case '`', '\'', '"':
token, next, complete := p.beginString(lineNo, column, line, pos)
if !complete {
return nil
}
p.addToken(token)
pos = next
case '{', '}', '=', ',', ':':
p.addToken(Token{Kind: TokenSymbol, Raw: line[pos : pos+1], Value: line[pos : pos+1], Line: lineNo, Column: column})
pos++
default:
raw, next := scanBare(line, pos)
if raw == "" {
pos++
continue
}
p.addToken(parseBare(lineNo, column, raw))
pos = next
}
}
return nil
}
func (p *docParser) beginString(lineNo, column int, line string, start int) (Token, int, bool) {
pending := &pendingString{quote: line[start], line: lineNo, column: column}
pending.raw.WriteByte(line[start])
next, complete := appendStringSegment(pending, line, start+1)
if !complete {
p.pending = pending
return Token{}, 0, false
}
return pending.token(), next, true
}
func (p *docParser) continueString(lineNo int, line string) error {
p.pending.raw.WriteByte('\n')
p.pending.value.WriteByte('\n')
next, complete := appendStringSegment(p.pending, line, 0)
if !complete {
return nil
}
token := p.pending.token()
p.pending = nil
p.addToken(token)
return p.scanValues(lineNo, next+1, line[next:])
}
func appendStringSegment(pending *pendingString, line string, start int) (int, bool) {
for pos := start; pos < len(line); pos++ {
value := line[pos]
pending.raw.WriteByte(value)
if value == pending.quote {
return pos + 1, true
}
if pending.quote != '`' && value == '\\' && pos+1 < len(line) {
pos++
pending.raw.WriteByte(line[pos])
pending.value.WriteByte(line[pos])
continue
}
pending.value.WriteByte(value)
}
return len(line), false
}
func (p *pendingString) token() Token {
return Token{
Kind: TokenString,
Raw: p.raw.String(),
Value: p.value.String(),
Line: p.line,
Column: p.column,
}
}
func scanBare(line string, start int) (string, int) {
pos := start
for pos < len(line) {
if isSpace(line[pos]) || strings.ContainsRune("{},:=", rune(line[pos])) {
break
}
if pos+1 < len(line) && line[pos] == '/' && line[pos+1] == '/' {
break
}
pos++
}
return line[start:pos], pos
}
func parseBare(lineNo, column int, raw string) Token {
if value, ok := parseInt(raw); ok {
return Token{Kind: TokenInt, Raw: raw, Value: raw, Int: value, Line: lineNo, Column: column}
}
if value, ok := parseFloat(raw); ok {
return Token{Kind: TokenFloat, Raw: raw, Value: raw, Float: value, Line: lineNo, Column: column}
}
return Token{Kind: TokenIdent, Raw: raw, Value: raw, Line: lineNo, Column: column}
}
func parseInt(raw string) (int64, bool) {
if strings.ContainsAny(raw, ".eE") {
return 0, false
}
value, err := strconv.ParseInt(raw, 10, 64)
return value, err == nil
}
func parseFloat(raw string) (float64, bool) {
if !strings.ContainsAny(raw, ".eE") {
return 0, false
}
value, err := strconv.ParseFloat(raw, 64)
return value, err == nil
}
func isSpace(value byte) bool {
return value == ' ' || value == '\t' || value == '\r'
}
@@ -0,0 +1,279 @@
package pvf
import (
"context"
"errors"
"testing"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestParseSectionsAndListEntries(t *testing.T) {
doc, err := Parse("equipment/equipment.lst", `
[equipment]
1001 `+"`equipment/weapon/sword.equ`"+`
1002 `+"`equipment/armor/coat.equ`"+`
[metadata]
rarity 3 1.5
`)
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(doc.Sections) != 2 {
t.Fatalf("expected 2 sections, got %+v", doc.Sections)
}
tokens, ok := doc.Section("equipment")
if !ok || len(tokens) != 4 {
t.Fatalf("expected equipment section tokens, got ok=%v tokens=%+v", ok, tokens)
}
entries := ParseList(doc)
if len(entries) != 2 {
t.Fatalf("expected list entries, got %+v", entries)
}
if entries[0].ID != 1001 || entries[0].Path != "equipment/weapon/sword.equ" {
t.Fatalf("unexpected first entry: %+v", entries[0])
}
}
func TestParseRejectsBadInput(t *testing.T) {
if _, err := Parse("", ""); !errors.Is(err, ErrPathRequired) {
t.Fatalf("expected ErrPathRequired, got %v", err)
}
_, err := Parse("bad.lst", "`missing")
if err == nil {
t.Fatalf("expected unclosed string error")
}
}
func TestDocumentValueHelpers(t *testing.T) {
doc, err := Parse("dungeon/test.dgn", `
[monsters]
1001 1002 `+"`monster/boss.mob`"+`
[maps]
`+"`map/a.map` `map/b.map`"+`
`)
if err != nil {
t.Fatalf("parse: %v", err)
}
if got := doc.Ints("monsters"); len(got) != 2 || got[0] != 1001 || got[1] != 1002 {
t.Fatalf("unexpected ints: %+v", got)
}
if got := doc.Texts("maps"); len(got) != 2 || got[0] != "map/a.map" || got[1] != "map/b.map" {
t.Fatalf("unexpected texts: %+v", got)
}
}
func TestParseMultilineBacktickString(t *testing.T) {
doc, err := Parse("skill/test.skl", "[explain]\n`first line\nsecond line`\n[required level]\n5\n")
if err != nil {
t.Fatalf("parse multiline string: %v", err)
}
value, ok := doc.Text("explain")
if !ok || value != "first line\nsecond line" {
t.Fatalf("multiline value = %q ok=%t", value, ok)
}
level, ok := doc.Int("required level")
if !ok || level != 5 {
t.Fatalf("required level = %d ok=%t", level, ok)
}
}
func TestBuildIndexFromMemorySource(t *testing.T) {
source := &memSource{
texts: map[string]string{
"equipment/equipment.lst": "1001 `equipment/weapon/sword.equ`\n",
"equipment/weapon/sword.equ": `
[name]
` + "`short sword`" + `
[attack]
12
`,
"skill/skill.lst": "2001 `skill/slash.skl`\n",
},
files: []platformpvf.File{
{ArchivePath: "equipment/equipment.lst"},
{ArchivePath: "equipment/weapon/sword.equ"},
{ArchivePath: "skill/skill.lst"},
},
reads: make(map[string]int),
}
index, err := Build(context.Background(), source, BuildOptions{
Lists: []string{"equipment/equipment.lst"},
})
if err != nil {
t.Fatalf("build index: %v", err)
}
if snapshot := index.Snapshot(); snapshot.Documents != 2 || snapshot.Lists != 1 || snapshot.Refs != 1 {
t.Fatalf("unexpected snapshot: %+v", snapshot)
}
path, ok := index.Resolve("equipment/equipment.lst", 1001)
if !ok || path != "equipment/weapon/sword.equ" {
t.Fatalf("unexpected resolved path: %q ok=%v", path, ok)
}
doc, ok := index.Document("./EQUIPMENT/weapon/sword.equ")
if !ok {
t.Fatalf("expected indexed sword document")
}
if _, ok := doc.Section("attack"); !ok {
t.Fatalf("expected attack section")
}
before := source.reads["equipment/weapon/sword.equ"]
if _, ok := index.Document("equipment/weapon/sword.equ"); !ok {
t.Fatalf("expected cached document")
}
if source.reads["equipment/weapon/sword.equ"] != before {
t.Fatalf("document lookup should not read source again")
}
}
func TestBuildIndexResolvesListRelativePaths(t *testing.T) {
source := &memSource{
texts: map[string]string{
"equipment/equipment.lst": "1001 `weapon/sword.equ`\n",
"equipment/weapon/sword.equ": `
[name]
` + "`relative sword`" + `
`,
},
files: []platformpvf.File{
{ArchivePath: "equipment/equipment.lst"},
{ArchivePath: "equipment/weapon/sword.equ"},
},
reads: make(map[string]int),
}
index, err := Build(context.Background(), source, BuildOptions{
Lists: []string{"equipment/equipment.lst"},
})
if err != nil {
t.Fatalf("build index: %v", err)
}
resolved, ok := index.Resolve("equipment/equipment.lst", 1001)
if !ok || resolved != "equipment/weapon/sword.equ" {
t.Fatalf("resolved = %q ok=%v", resolved, ok)
}
if _, ok := index.Document("equipment/weapon/sword.equ"); !ok {
t.Fatalf("expected relative document to be indexed")
}
}
func TestBuildIndexLoadsNestedLists(t *testing.T) {
source := &memSource{
texts: map[string]string{
"skill/skilllist.lst": "0 `SwordmanSkill.lst`\n",
"skill/SwordmanSkill.lst": "46 `Swordman/UpperSlash.skl`\n",
"skill/Swordman/UpperSlash.skl": `
[name]
` + "`upper slash`" + `
`,
},
files: []platformpvf.File{
{ArchivePath: "skill/skilllist.lst"},
{ArchivePath: "skill/SwordmanSkill.lst"},
{ArchivePath: "skill/Swordman/UpperSlash.skl"},
},
reads: make(map[string]int),
}
index, err := Build(context.Background(), source, BuildOptions{Lists: []string{"skill/skilllist.lst"}})
if err != nil {
t.Fatalf("build nested index: %v", err)
}
if snapshot := index.Snapshot(); snapshot.Documents != 3 || snapshot.Lists != 2 || snapshot.Refs != 2 {
t.Fatalf("unexpected nested snapshot: %+v", snapshot)
}
jobList, ok := index.Resolve("skill/skilllist.lst", 0)
if !ok || jobList != "skill/SwordmanSkill.lst" {
t.Fatalf("job list = %q ok=%t", jobList, ok)
}
skillPath, ok := index.Resolve(jobList, 46)
if !ok || skillPath != "skill/Swordman/UpperSlash.skl" {
t.Fatalf("skill path = %q ok=%t", skillPath, ok)
}
if _, ok := index.Document(skillPath); !ok {
t.Fatal("nested skill document was not loaded")
}
}
func TestBuildIndexRequiresSource(t *testing.T) {
_, err := Build(context.Background(), nil, BuildOptions{})
if !errors.Is(err, ErrSourceRequired) {
t.Fatalf("expected ErrSourceRequired, got %v", err)
}
}
func TestBuildIndexHonorsCanceledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := Build(ctx, &memSource{
texts: map[string]string{"a.lst": "1 `a/a.equ`"},
reads: make(map[string]int),
}, BuildOptions{Paths: []string{"a.lst"}})
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected context canceled, got %v", err)
}
}
func TestBuildIndexUsesDirectLookupWithoutListingArchive(t *testing.T) {
source := &finderMemSource{memSource: memSource{
texts: map[string]string{
"skill/skill.lst": "1 `relative.skl`\n",
"skill/relative.skl": "[name]\n`direct lookup`\n",
},
files: []platformpvf.File{
{ArchivePath: "skill/skill.lst"},
{ArchivePath: "skill/relative.skl"},
},
reads: make(map[string]int),
}}
index, err := Build(context.Background(), source, BuildOptions{Lists: []string{"skill/skill.lst"}})
if err != nil {
t.Fatalf("build index: %v", err)
}
if source.listCalls != 0 {
t.Fatalf("Files calls=%d want=0", source.listCalls)
}
if _, ok := index.Document("skill/relative.skl"); !ok {
t.Fatal("relative document was not loaded through direct lookup")
}
}
type memSource struct {
texts map[string]string
files []platformpvf.File
reads map[string]int
}
type finderMemSource struct {
memSource
listCalls int
}
func (s *finderMemSource) Files() []platformpvf.File {
s.listCalls++
return s.memSource.Files()
}
func (s *finderMemSource) FindFile(relativePath string) (platformpvf.File, bool) {
key := pathKey(relativePath)
for _, file := range s.files {
if pathKey(file.ArchivePath) == key {
return file, true
}
}
return platformpvf.File{}, false
}
func (s *memSource) ReadText(relativePath string) (string, error) {
key := pathKey(relativePath)
for path, text := range s.texts {
if pathKey(path) == key {
s.reads[cleanPath(path)]++
return text, nil
}
}
return "", platformpvf.ErrFileNotFound
}
func (s *memSource) Files() []platformpvf.File {
return append([]platformpvf.File(nil), s.files...)
}
+47
View File
@@ -0,0 +1,47 @@
package pvf
import (
"path"
"strings"
)
func cleanPath(value string) string {
value = strings.TrimSpace(strings.ReplaceAll(value, "\\", "/"))
for strings.HasPrefix(value, "./") || strings.HasPrefix(value, "/") {
value = strings.TrimPrefix(value, "./")
value = strings.TrimPrefix(value, "/")
}
if value == "" {
return ""
}
clean := strings.TrimSuffix(path.Clean(value), "/")
if clean == "." {
return ""
}
return clean
}
func pathKey(value string) string {
return strings.ToLower(cleanPath(value))
}
func sectionKey(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func matchPrefix(filePath string, prefixes []string) bool {
if len(prefixes) == 0 {
return false
}
key := pathKey(filePath)
for _, prefix := range prefixes {
prefixKey := pathKey(prefix)
if prefixKey == "" {
continue
}
if key == prefixKey || strings.HasPrefix(key, prefixKey+"/") {
return true
}
}
return false
}
+84
View File
@@ -0,0 +1,84 @@
package pvf
import "errors"
var (
ErrSourceRequired = errors.New("dnf pvf source is required")
ErrPathRequired = errors.New("dnf pvf path is required")
ErrDocNotFound = errors.New("dnf pvf document not found")
)
type TokenKind string
const (
// TokenSection 表示 PVF 文本中的 `[section]` 段落标记。
TokenSection TokenKind = "section"
// TokenString 表示反引号、单引号或双引号包裹的文本值。
TokenString TokenKind = "string"
// TokenInt 表示整数值。
TokenInt TokenKind = "int"
// TokenFloat 表示浮点值。
TokenFloat TokenKind = "float"
// TokenIdent 表示未加引号的普通标识符。
TokenIdent TokenKind = "ident"
// TokenSymbol 表示结构符号,例如 `{`、`}`、`=`。
TokenSymbol TokenKind = "symbol"
)
// Token 表示 PVF 文本解析后的最小节点。
type Token struct {
Kind TokenKind `json:"kind"`
Raw string `json:"raw"`
Value string `json:"value,omitempty"`
Int int64 `json:"int,omitempty"`
Float float64 `json:"float,omitempty"`
Line int `json:"line"`
Column int `json:"column"`
}
// Section 表示一个 `[section]` 段及其 token 范围。
type Section struct {
Name string `json:"name"`
Start int `json:"start"`
End int `json:"end"`
}
// Document 表示单个 PVF 文本文档的解析结果。
type Document struct {
Path string `json:"path"`
Tokens []Token `json:"tokens"`
Sections []Section `json:"sections"`
}
// ListEntry 表示 `.lst` 中的 id 到 PVF 路径引用。
type ListEntry struct {
ID int64 `json:"id"`
Path string `json:"path"`
}
// Snapshot 表示 DNF PVF 内存索引的简要状态。
type Snapshot struct {
Documents int `json:"documents"`
Lists int `json:"lists"`
Refs int `json:"refs"`
}
// Section 返回指定段落内的 token 副本。
func (d *Document) Section(name string) ([]Token, bool) {
if d == nil {
return nil, false
}
key := sectionKey(name)
for _, section := range d.Sections {
if sectionKey(section.Name) != key {
continue
}
if section.Start < 0 || section.End > len(d.Tokens) || section.Start > section.End {
return nil, false
}
out := make([]Token, section.End-section.Start)
copy(out, d.Tokens[section.Start:section.End])
return out, true
}
return nil, false
}
+76
View File
@@ -0,0 +1,76 @@
package pvf
// Text 返回指定 section 的第一个文本或标识符值。
func (d *Document) Text(name string) (string, bool) {
values := d.Texts(name)
if len(values) == 0 {
return "", false
}
return values[0], true
}
// Int 返回指定 section 的第一个整数值。
func (d *Document) Int(name string) (int64, bool) {
values := d.Ints(name)
if len(values) == 0 {
return 0, false
}
return values[0], true
}
// Number 返回指定 section 的第一个数字值。
func (d *Document) Number(name string) (float64, bool) {
values := d.Numbers(name)
if len(values) == 0 {
return 0, false
}
return values[0], true
}
// Texts 返回指定 section 内所有文本和标识符值。
func (d *Document) Texts(name string) []string {
tokens, ok := d.Section(name)
if !ok {
return nil
}
out := make([]string, 0, len(tokens))
for _, token := range tokens {
if token.Kind == TokenString || token.Kind == TokenIdent {
out = append(out, token.Value)
}
}
return out
}
// Ints 返回指定 section 内所有整数值。
func (d *Document) Ints(name string) []int64 {
tokens, ok := d.Section(name)
if !ok {
return nil
}
out := make([]int64, 0, len(tokens))
for _, token := range tokens {
if token.Kind == TokenInt {
out = append(out, token.Int)
}
}
return out
}
// Numbers 返回指定 section 内所有数字值。
func (d *Document) Numbers(name string) []float64 {
tokens, ok := d.Section(name)
if !ok {
return nil
}
out := make([]float64, 0, len(tokens))
for _, token := range tokens {
switch token.Kind {
case TokenInt:
out = append(out, float64(token.Int))
case TokenFloat:
out = append(out, token.Float)
}
}
return out
}