package extract import ( "context" "fmt" "io" "os" "path/filepath" "strings" ) type entryWriter struct { ctx context.Context root string limits Limits progress Progress written int64 entries int } func (writer *entryWriter) writeDirectory(name string) error { target, err := writer.prepare(name) if err != nil || target == "" { return err } return os.MkdirAll(target, 0o755) } func (writer *entryWriter) writeFile(name string, content io.Reader) error { target, err := writer.prepare(name) if err != nil || target == "" { return err } if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return err } file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) if err != nil { return err } remaining := writer.limits.MaxBytes - writer.written source := &progressReader{ctx: writer.ctx, reader: io.LimitReader(content, remaining+1), progress: writer.progress} copied, copyErr := io.Copy(file, source) closeErr := file.Close() writer.written += copied if copyErr != nil { return copyErr } if writer.written > writer.limits.MaxBytes { return ErrTooLarge } return closeErr } func (writer *entryWriter) prepare(name string) (string, error) { writer.entries++ if writer.entries > writer.limits.MaxEntries { return "", ErrTooManyEntries } if err := writer.ctx.Err(); err != nil { return "", err } return writer.resolve(name) } func (writer *entryWriter) resolve(name string) (string, error) { normalized := strings.ReplaceAll(name, "\\", "/") if strings.HasPrefix(normalized, "/") { return "", fmt.Errorf("%w: %s", ErrUnsafeEntry, name) } cleaned := filepath.Clean(filepath.FromSlash(normalized)) if cleaned == "." { return "", nil } if !filepath.IsLocal(cleaned) { return "", fmt.Errorf("%w: %s", ErrUnsafeEntry, name) } target := filepath.Join(writer.root, cleaned) relative, err := filepath.Rel(writer.root, target) if err != nil || !filepath.IsLocal(relative) { return "", fmt.Errorf("%w: %s", ErrUnsafeEntry, name) } return target, nil } type progressReader struct { ctx context.Context reader io.Reader progress Progress } func (reader *progressReader) Read(buffer []byte) (int, error) { if err := reader.ctx.Err(); err != nil { return 0, err } count, err := reader.reader.Read(buffer) if count > 0 { reader.progress.Add(int64(count)) } return count, err }