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 }