package handlers import ( "archive/zip" "bytes" "context" "errors" "io" "log/slog" "path/filepath" "regexp" "strconv" "strings" filemanager "wucher/internal/domain/file_manager" ) var ( xlsxVMLStyleAttrRE = regexp.MustCompile(`(?i)(style\s*=\s*["'])([^"']*)(["'])`) xlsxVMLLengthTokenRE = regexp.MustCompile(`^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:pt|px|in|cm|mm|pc|em|ex|%)$`) xlsxVMLShapeTagRE = regexp.MustCompile(`(?is)]*>`) xlsxVMLShapeIDAttrRE = regexp.MustCompile(`(?i)(\bid\s*=\s*["'])(_x0000_s\d+)(["'])`) xlsxVMLShapeIDNumRE = regexp.MustCompile(`(?i)^_x0000_s(\d+)$`) xlsxVMLShapeTypeRE = regexp.MustCompile(`(?i)]*>`) ) // xlsxVMLNoteShapeTypeHeader is the standard / // preamble that Excel writes at the top of every comment/note VML drawing. // Some producers (and files that have been round-tripped through other tools) // omit it while still emitting shapes that reference type="#_x0000_t202"; // LibreOffice/Collabora then flags the package as corrupt and offers a repair. // Injecting the missing definition makes the shape reference resolve and // removes the repair prompt. const xlsxVMLNoteShapeTypeHeader = `` + `` + `` type xlsxSanitizerStorage interface { GetObject(context.Context, string) (io.ReadCloser, filemanager.ObjectMetadata, error) PutObject(context.Context, filemanager.PutObjectInput) (filemanager.ObjectMetadata, error) } type xlsxSanitizerFileUpdater interface { UpdateFile(context.Context, *filemanager.File) error } func sanitizeXLSXPayload(payload []byte) ([]byte, int, bool, error) { reader, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload))) if err != nil { return nil, 0, false, errors.New("file content is not a valid office zip document") } var buf bytes.Buffer writer := zip.NewWriter(&buf) if reader.Comment != "" { if err := writer.SetComment(reader.Comment); err != nil { _ = writer.Close() return nil, 0, false, err } } totalStyleFixes := 0 changed := false for _, file := range reader.File { header := file.FileHeader header.Name = file.Name header.Comment = file.Comment header.NonUTF8 = file.NonUTF8 if file.FileInfo().IsDir() { entry, err := writer.CreateHeader(&header) if err != nil { _ = writer.Close() return nil, 0, false, err } if _, err := entry.Write(nil); err != nil { _ = writer.Close() return nil, 0, false, err } continue } if !isVMLDrawingEntry(file.Name) { raw, err := file.OpenRaw() if err != nil { _ = writer.Close() return nil, 0, false, err } entry, err := writer.CreateRaw(&header) if err != nil { _ = writer.Close() return nil, 0, false, err } if _, err := io.Copy(entry, raw); err != nil { _ = writer.Close() return nil, 0, false, err } continue } rc, err := file.Open() if err != nil { _ = writer.Close() return nil, 0, false, err } filePayload, readErr := io.ReadAll(rc) _ = rc.Close() if readErr != nil { _ = writer.Close() return nil, 0, false, readErr } sanitizedPayload, styleFixes, fileChanged, err := sanitizeVMLDrawingPayload(filePayload) if err != nil { _ = writer.Close() return nil, 0, false, err } if dedupedPayload, idFixes, idChanged := dedupeVMLShapeIDs(sanitizedPayload); idChanged { sanitizedPayload = dedupedPayload fileChanged = true totalStyleFixes += idFixes } if shapeTypePayload, shapeTypeFixes, shapeTypeChanged := ensureVMLNoteShapeType(sanitizedPayload); shapeTypeChanged { sanitizedPayload = shapeTypePayload fileChanged = true totalStyleFixes += shapeTypeFixes } if fileChanged { changed = true totalStyleFixes += styleFixes } if !fileChanged { raw, err := file.OpenRaw() if err != nil { _ = writer.Close() return nil, 0, false, err } entry, err := writer.CreateRaw(&header) if err != nil { _ = writer.Close() return nil, 0, false, err } if _, err := io.Copy(entry, raw); err != nil { _ = writer.Close() return nil, 0, false, err } continue } entry, err := writer.CreateHeader(&header) if err != nil { _ = writer.Close() return nil, 0, false, err } if _, err := entry.Write(sanitizedPayload); err != nil { _ = writer.Close() return nil, 0, false, err } } if err := writer.Close(); err != nil { return nil, 0, false, err } if !changed { return payload, 0, false, nil } return buf.Bytes(), totalStyleFixes, true, nil } func sanitizeVMLDrawingPayload(payload []byte) ([]byte, int, bool, error) { totalFixes := 0 changed := false rewritten := xlsxVMLStyleAttrRE.ReplaceAllFunc(payload, func(match []byte) []byte { sub := xlsxVMLStyleAttrRE.FindSubmatch(match) if len(sub) != 4 { return match } normalized, styleFixes, fileChanged := sanitizeVMLStyleValue(string(sub[2])) if !fileChanged { return match } totalFixes += styleFixes changed = true out := make([]byte, 0, len(sub[1])+len(normalized)+len(sub[3])) out = append(out, sub[1]...) out = append(out, normalized...) out = append(out, sub[3]...) return out }) if !changed { return payload, 0, false, nil } return rewritten, totalFixes, true, nil } func dedupeVMLShapeIDs(payload []byte) ([]byte, int, bool) { if len(payload) == 0 { return payload, 0, false } source := string(payload) tags := xlsxVMLShapeTagRE.FindAllStringIndex(source, -1) if len(tags) == 0 { return payload, 0, false } maxID := 0 for _, match := range xlsxVMLShapeIDAttrRE.FindAllStringSubmatch(source, -1) { if len(match) < 3 { continue } if sub := xlsxVMLShapeIDNumRE.FindStringSubmatch(match[2]); len(sub) == 2 { if n, err := strconv.Atoi(sub[1]); err == nil && n > maxID { maxID = n } } } if maxID == 0 { return payload, 0, false } seen := make(map[string]struct{}, len(tags)) var buf strings.Builder buf.Grow(len(source) + len(tags)*8) last := 0 nextID := maxID + 1 fixCount := 0 for _, loc := range tags { tag := source[loc[0]:loc[1]] newTag, changed := dedupeVMLShapeIDTag(tag, seen, &nextID) if changed { fixCount++ } buf.WriteString(source[last:loc[0]]) buf.WriteString(newTag) last = loc[1] } buf.WriteString(source[last:]) if fixCount == 0 { return payload, 0, false } return []byte(buf.String()), fixCount, true } func dedupeVMLShapeIDTag(tag string, seen map[string]struct{}, nextID *int) (string, bool) { match := xlsxVMLShapeIDAttrRE.FindStringSubmatchIndex(tag) if len(match) < 8 { return tag, false } currentID := tag[match[4]:match[5]] if _, ok := seen[currentID]; !ok { seen[currentID] = struct{}{} return tag, false } if nextID == nil { return tag, false } newID := "_x0000_s" + strconv.Itoa(*nextID) *nextID = *nextID + 1 return tag[:match[4]] + newID + tag[match[5]:], true } // ensureVMLNoteShapeType injects the standard note definition // (and its preamble) when the drawing references // type="#_x0000_t202" but never defines it. Returns the payload unchanged when // a shapetype already exists, when no note shape references it, or when the // root element lacks the o:/v: namespaces the injected markup needs. func ensureVMLNoteShapeType(payload []byte) ([]byte, int, bool) { if len(payload) == 0 { return payload, 0, false } if xlsxVMLShapeTypeRE.Match(payload) { return payload, 0, false } if !xlsxVMLNoteTypeRefRE.Match(payload) { return payload, 0, false } root := xlsxVMLRootTagRE.FindIndex(payload) if root == nil { return payload, 0, false } rootTag := bytes.ToLower(payload[root[0]:root[1]]) if !bytes.Contains(rootTag, []byte("xmlns:o")) || !bytes.Contains(rootTag, []byte("xmlns:v")) { return payload, 0, false } out := make([]byte, 0, len(payload)+len(xlsxVMLNoteShapeTypeHeader)) out = append(out, payload[:root[1]]...) out = append(out, xlsxVMLNoteShapeTypeHeader...) out = append(out, payload[root[1]:]...) return out, 1, true } func sanitizeVMLStyleValue(raw string) (string, int, bool) { trimmed := strings.TrimSpace(raw) if trimmed == "" { return raw, 0, false } tokens := strings.Split(trimmed, ";") if len(tokens) == 0 { return raw, 0, false } positionIndex := -1 for i := range tokens { if isVMLAbsolutePositionToken(tokens[i]) { positionIndex = i break } } if positionIndex < 0 { return raw, 0, false } leadingLengths := make([]string, 0, 2) nextIndex := positionIndex + 1 for nextIndex < len(tokens) && len(leadingLengths) < 2 { token := strings.TrimSpace(tokens[nextIndex]) if token == "" { nextIndex++ continue } if !isVMLLengthToken(token) { break } leadingLengths = append(leadingLengths, token) nextIndex++ } if len(leadingLengths) == 0 { return raw, 0, false } remaining := make([]string, 0, len(tokens)-nextIndex+3) for ; nextIndex < len(tokens); nextIndex++ { token := strings.TrimSpace(tokens[nextIndex]) if token == "" { continue } remaining = append(remaining, token) } styleParts := make([]string, 0, len(tokens)+2) for i := 0; i < positionIndex; i++ { token := strings.TrimSpace(tokens[i]) if token == "" { continue } styleParts = append(styleParts, token) } styleParts = append(styleParts, "position:absolute") styleParts = append(styleParts, "margin-left:"+leadingLengths[0]) if len(leadingLengths) >= 2 { styleParts = append(styleParts, "margin-top:"+leadingLengths[1]) } else { styleParts = append(styleParts, "margin-top:0pt") } styleParts = append(styleParts, remaining...) return strings.Join(styleParts, ";"), len(leadingLengths), true } func isVMLDrawingEntry(name string) bool { name = strings.ToLower(strings.TrimSpace(strings.ReplaceAll(name, "\\", "/"))) if name == "" { return false } return strings.HasPrefix(name, "xl/drawings/vmldrawing") && strings.HasSuffix(name, ".vml") } func isVMLAbsolutePositionToken(token string) bool { token = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(token), " ", "")) return token == "position:absolute" } func isVMLLengthToken(token string) bool { return xlsxVMLLengthTokenRE.MatchString(strings.ToLower(strings.TrimSpace(token))) } func (h *FileManagerFileHandler) sanitizeStoredXLSXObject(ctx context.Context, row *filemanager.File, objectKey, nameHint, mimeHint, operation string) (bool, int64, error) { if h == nil || h.storage == nil { return false, 0, nil } storage, ok := any(h.storage).(xlsxSanitizerStorage) if !ok { return false, 0, errors.New("storage does not support office sanitization") } return sanitizeStoredXLSXObject(ctx, storage, h.svc, row, objectKey, nameHint, mimeHint, operation) } func sanitizeStoredXLSXObject( ctx context.Context, storage xlsxSanitizerStorage, updater xlsxSanitizerFileUpdater, row *filemanager.File, objectKey, nameHint, mimeHint, operation string, ) (bool, int64, error) { if storage == nil { return false, 0, nil } if ctx == nil { ctx = context.Background() } objectKey = strings.TrimSpace(objectKey) if objectKey == "" && row != nil { objectKey = strings.TrimSpace(row.ObjectKey) } if objectKey == "" { return false, 0, nil } name := strings.TrimSpace(nameHint) if name == "" && row != nil { name = strings.TrimSpace(row.Name) } ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), ".")) if ext == "" { ext = strings.ToLower(strings.TrimPrefix(filepath.Ext(objectKey), ".")) } if ext != "xlsx" { return false, 0, nil } body, meta, err := storage.GetObject(ctx, objectKey) if err != nil { return false, 0, err } defer body.Close() payload, err := io.ReadAll(body) if err != nil { return false, 0, err } sanitizedPayload, styleFixes, changed, err := sanitizeXLSXPayload(payload) if err != nil { return false, 0, err } if !changed { return false, int64(len(payload)), nil } contentType := strings.TrimSpace(firstNonEmpty(mimeHint, meta.ContentType, canonicalOfficeContentType(name))) if contentType == "" { contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" } newMeta, err := storage.PutObject(ctx, filemanager.PutObjectInput{ ObjectKey: objectKey, ContentType: contentType, SizeBytes: int64(len(sanitizedPayload)), Body: bytes.NewReader(sanitizedPayload), }) if err != nil { return true, int64(len(sanitizedPayload)), err } logArgs := []any{ slog.String("operation", strings.TrimSpace(operation)), slog.String("object_key", objectKey), slog.String("mime_type", contentType), slog.Int("style_fix_count", styleFixes), slog.Int("before_bytes", len(payload)), slog.Int("after_bytes", len(sanitizedPayload)), } if row != nil { logArgs = append(logArgs, slog.String("file_id", idString(row.ID))) logArgs = append(logArgs, slog.String("file_name", strings.TrimSpace(row.Name))) } slog.WarnContext(ctx, "sanitized malformed xlsx VML drawing", logArgs...) if row != nil { row.SizeBytes = int64(len(sanitizedPayload)) row.MimeType = contentType if strings.TrimSpace(newMeta.Bucket) != "" { row.Bucket = newMeta.Bucket } if strings.TrimSpace(newMeta.ObjectKey) != "" { row.ObjectKey = newMeta.ObjectKey } if strings.TrimSpace(newMeta.ETag) != "" { row.ETag = newMeta.ETag } if strings.TrimSpace(newMeta.VersionID) != "" { row.ObjectVersion = newMeta.VersionID } if updater != nil { if err := updater.UpdateFile(ctx, row); err != nil { return true, int64(len(sanitizedPayload)), err } } } return true, int64(len(sanitizedPayload)), nil }