303 lines
8.2 KiB
Go
303 lines
8.2 KiB
Go
package app
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"ric930-usrinfo/internal/usr"
|
|
"ric930-usrinfo/internal/xmlcheck"
|
|
)
|
|
|
|
type Mode string
|
|
|
|
const (
|
|
ModeDecode Mode = "decode"
|
|
ModeEncode Mode = "encode"
|
|
)
|
|
|
|
type Config struct {
|
|
Dir string
|
|
MaxXMLBytes int64
|
|
NoOverwrite bool
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
}
|
|
|
|
type Result struct {
|
|
Found int
|
|
Succeeded int
|
|
Skipped int
|
|
Failed int
|
|
}
|
|
|
|
func (r Result) ExitCode() int {
|
|
if r.Failed > 0 {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func Run(cfg Config) (Result, error) {
|
|
return RunCommand(cfg, ModeDecode, nil)
|
|
}
|
|
|
|
func RunCommand(cfg Config, mode Mode, selectors []string) (Result, error) {
|
|
var result Result
|
|
if cfg.Dir == "" {
|
|
cfg.Dir = "."
|
|
}
|
|
if cfg.Stdout == nil {
|
|
cfg.Stdout = io.Discard
|
|
}
|
|
if cfg.Stderr == nil {
|
|
cfg.Stderr = io.Discard
|
|
}
|
|
if cfg.MaxXMLBytes <= 0 {
|
|
return result, fmt.Errorf("некорректный лимит размера XML")
|
|
}
|
|
if mode != ModeDecode && mode != ModeEncode {
|
|
return result, fmt.Errorf("неизвестная команда %q", mode)
|
|
}
|
|
if mode == ModeEncode && len(selectors) == 0 {
|
|
return result, fmt.Errorf("для encode необходимо указать XML-файлы или маски")
|
|
}
|
|
|
|
var files []string
|
|
if selectors == nil && mode == ModeDecode {
|
|
entries, err := os.ReadDir(cfg.Dir)
|
|
if err != nil {
|
|
return result, fmt.Errorf("не удалось прочитать каталог %q: %w", cfg.Dir, err)
|
|
}
|
|
for _, entry := range entries {
|
|
if !strings.EqualFold(filepath.Ext(entry.Name()), ".usr") {
|
|
continue
|
|
}
|
|
if entry.Type().IsRegular() {
|
|
files = append(files, filepath.Join(cfg.Dir, entry.Name()))
|
|
} else if entry.Type()&os.ModeType == 0 {
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
fmt.Fprintf(cfg.Stderr, "[ERROR] %s: невозможно получить атрибуты: %v\n", entry.Name(), err)
|
|
result.Failed++
|
|
} else if info.Mode().IsRegular() {
|
|
files = append(files, filepath.Join(cfg.Dir, entry.Name()))
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if len(selectors) == 0 {
|
|
return result, fmt.Errorf("для %s необходимо указать хотя бы один файл или маску", mode)
|
|
}
|
|
seen := make(map[string]bool)
|
|
for _, selector := range selectors {
|
|
if selector == "" {
|
|
fmt.Fprintln(cfg.Stderr, "[ERROR] пустая маска")
|
|
result.Failed++
|
|
continue
|
|
}
|
|
pattern := selector
|
|
if !filepath.IsAbs(selector) {
|
|
pattern = filepath.Join(cfg.Dir, selector)
|
|
}
|
|
var matches []string
|
|
var err error
|
|
if strings.ContainsAny(selector, "*?[") {
|
|
matches, err = filepath.Glob(pattern)
|
|
} else {
|
|
matches = []string{pattern}
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(cfg.Stderr, "[ERROR] неверная маска %q: %v\n", selector, err)
|
|
result.Failed++
|
|
continue
|
|
}
|
|
if len(matches) == 0 {
|
|
fmt.Fprintf(cfg.Stderr, "[ERROR] по маске %q файлы не найдены\n", selector)
|
|
result.Failed++
|
|
continue
|
|
}
|
|
for _, match := range matches {
|
|
clean := filepath.Clean(match)
|
|
if !seen[clean] {
|
|
seen[clean] = true
|
|
files = append(files, clean)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Found = len(files)
|
|
if len(files) == 0 {
|
|
if result.Failed == 0 {
|
|
fmt.Fprintln(cfg.Stdout, "Подходящие файлы не найдены.")
|
|
}
|
|
return result, nil
|
|
}
|
|
fmt.Fprintf(cfg.Stdout, "Выбрано файлов (%s): %d\n", mode, result.Found)
|
|
|
|
outputs := make(map[string]int)
|
|
for _, path := range files {
|
|
base := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
|
if base == "" {
|
|
continue
|
|
}
|
|
stem := filepath.Join(filepath.Dir(path), base)
|
|
outputs[strings.ToLower(filepath.Clean(stem))]++
|
|
}
|
|
for _, path := range files {
|
|
ext := filepath.Ext(path)
|
|
base := strings.TrimSuffix(filepath.Base(path), ext)
|
|
stem := filepath.Join(filepath.Dir(path), base)
|
|
if base == "" || outputs[strings.ToLower(filepath.Clean(stem))] > 1 {
|
|
fmt.Fprintf(cfg.Stderr, "[ERROR] %s: несколько входных файлов дают одинаковое имя результата\n", path)
|
|
result.Failed++
|
|
continue
|
|
}
|
|
if (mode == ModeDecode && !strings.EqualFold(ext, ".usr")) ||
|
|
(mode == ModeEncode && !strings.EqualFold(ext, ".xml")) {
|
|
fmt.Fprintf(cfg.Stderr, "[ERROR] %s: неподдерживаемое расширение для %s\n", path, mode)
|
|
result.Failed++
|
|
continue
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
fmt.Fprintf(cfg.Stderr, "[ERROR] %s: %v\n", path, err)
|
|
result.Failed++
|
|
continue
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
fmt.Fprintf(cfg.Stderr, "[ERROR] %s: требуется обычный файл (не каталог и не символьная ссылка)\n", path)
|
|
result.Failed++
|
|
continue
|
|
}
|
|
if mode == ModeDecode {
|
|
err = decodeOne(cfg, path, stem)
|
|
} else {
|
|
err = encodeOne(cfg, path, stem)
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, errSkip) {
|
|
fmt.Fprintf(cfg.Stdout, "[SKIP] %s: результат уже существует (-no-overwrite)\n", path)
|
|
result.Skipped++
|
|
} else {
|
|
fmt.Fprintf(cfg.Stderr, "[ERROR] %s: %v\n", path, err)
|
|
result.Failed++
|
|
}
|
|
continue
|
|
}
|
|
result.Succeeded++
|
|
if mode == ModeDecode {
|
|
fmt.Fprintf(cfg.Stdout, "[OK] %s -> %s.XML\n", path, stem)
|
|
} else {
|
|
fmt.Fprintf(cfg.Stdout, "[OK] %s -> %s.USR\n", path, stem)
|
|
}
|
|
}
|
|
fmt.Fprintf(cfg.Stdout, "Итого: готово %d, пропущено %d, ошибок %d\n", result.Succeeded, result.Skipped, result.Failed)
|
|
return result, nil
|
|
}
|
|
|
|
var errSkip = errors.New("выходной файл уже существует")
|
|
|
|
func checkOutputs(paths ...string) error {
|
|
for _, path := range paths {
|
|
if _, err := os.Lstat(path); err == nil {
|
|
return errSkip
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("не удалось проверить %s: %w", path, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeOne(cfg Config, path, stem string) error {
|
|
output := stem + ".XML"
|
|
if cfg.NoOverwrite {
|
|
if err := checkOutputs(output); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("проверка USR: %w", err)
|
|
}
|
|
if info.Size() > usr.MaxInputBytes {
|
|
return fmt.Errorf("USR превышает предел %d байт", usr.MaxInputBytes)
|
|
}
|
|
encoded, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("чтение USR: %w", err)
|
|
}
|
|
xmlData, err := usr.Decode(encoded, cfg.MaxXMLBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("декодирование: %w", err)
|
|
}
|
|
if err := xmlcheck.Validate(xmlData); err != nil {
|
|
return fmt.Errorf("распакованные данные не являются корректным XML: %w", err)
|
|
}
|
|
return writeFile(output, xmlData)
|
|
}
|
|
|
|
func encodeOne(cfg Config, path, stem string) error {
|
|
output := stem + ".USR"
|
|
if cfg.NoOverwrite {
|
|
if err := checkOutputs(output); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("проверка XML: %w", err)
|
|
}
|
|
if info.Size() > cfg.MaxXMLBytes {
|
|
return fmt.Errorf("XML превышает предел %d байт", cfg.MaxXMLBytes)
|
|
}
|
|
xmlData, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("чтение XML: %w", err)
|
|
}
|
|
if err := xmlcheck.Validate(xmlData); err != nil {
|
|
return fmt.Errorf("некорректный исходный XML: %w", err)
|
|
}
|
|
encoded, err := usr.Encode(xmlData, cfg.MaxXMLBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("кодирование USR: %w", err)
|
|
}
|
|
return writeFile(output, encoded)
|
|
}
|
|
|
|
func writeFile(path string, data []byte) error {
|
|
temp, err := createTempFile(filepath.Dir(path), filepath.Base(path), data)
|
|
if err != nil {
|
|
return fmt.Errorf("подготовка файла %s: %w", path, err)
|
|
}
|
|
defer os.Remove(temp)
|
|
if err := os.Rename(temp, path); err != nil {
|
|
return fmt.Errorf("сохранение файла %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createTempFile(dir, name string, data []byte) (string, error) {
|
|
f, err := os.CreateTemp(dir, "."+name+"-*")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
path := f.Name()
|
|
if _, err = f.Write(data); err == nil {
|
|
err = f.Sync()
|
|
}
|
|
closeErr := f.Close()
|
|
if err == nil {
|
|
err = closeErr
|
|
}
|
|
if err != nil {
|
|
os.Remove(path)
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|