blob: e41d544bae99226e58554b272019b5a383893ee5 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package wrap is used to wrap comments/edits made to GitHub so that
// they can later be identified without referencing a database.
// This is especially useful for stripping out edits to a user-generated
// post made by a bot.
//
// DO NOT REMOVE/EDIT ANY LITERAL STRINGS OR FORMATTING USED IN THIS PACKAGE,
// (unless you have a good reason and a plan to migrate any existing usage).
// Go names can be changed.
//
// Wrapped strings ($body) are of the form:
//
// <!-- Generated by Oscar. DO NOT EDIT. {"bot":"$bot","kind":"$kind","meta":$meta} -->$body<!-- oscar-end -->
//
// where $bot and $kind are specified in [New], and $meta is arbitrary
// metadata (if nil, metadata is omitted).
//
// Note that the wrapping strategy is not robust against a user
// intentionally removing or editing the tag(s).
package wrap
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"golang.org/x/oscar/internal/storage"
)
// A Wrapper is used to wrap comments/edits made to GitHub so that
// they can later be identified without referencing a database.
type Wrapper struct {
bot, kind string
}
// New returns a wrapper for GitHub modifications.
// bot is the name of the bot (e.g. "gabyhelp") that is making the edits,
// and kind is a context string used to identify the purpose/type
// of the edit (e.g "overview" or "related").
func New(bot, kind string) *Wrapper {
return &Wrapper{
bot: bot, kind: kind,
}
}
// Wrap wraps body (the text of a GitHub modification).
// body must not already be wrapped, or contain the end tag (<!-- oscar-end -->).
// metadata is freeform metadata to include in the hidden tags.
// metadata is sanitized via [json.Marshal], so it may contain otherwise
// forbidden elements (e.g., "-->").
func (w *Wrapper) Wrap(body string, metadata any) (string, error) {
// Body cannot contain the string [endTag].
if strings.Contains(body, endTag) {
return "", fmt.Errorf("github: wrapped body cannot contain %q", endTag)
}
// DO NOT REMOVE/EDIT STRUCTURE OR CONTENTS.
return w.startTag(metadata) + body + endTag, nil
}
// The text to inclue in the start tag before the metadata.
// DO NOT REMOVE/EDIT.
const tagText = "Generated by Oscar. DO NOT EDIT."
// regexps for checking if a string is wrapped
var (
reString = `<!-- ` + regexp.QuoteMeta(tagText) + ` (\{.*?\}) -->(?s)(.*?)<!-- oscar-end -->`
containsRE = regexp.MustCompile(reString)
isRE = regexp.MustCompile(`^` + reString + `$`)
)
// Is reports whether the string is of the form
// returned by [Wrapper.Wrap] (for any [Wrapper]).
func Is(s string) bool {
return isRE.MatchString(s)
}
// Contains returns whether the given string
// contains one or more strings of the form output by
// [Wrapper.Wrap] (for any [Wrapper]).
func Contains(s string) bool {
return containsRE.MatchString(s)
}
// Unwrapped contains the parsed contents of a wrapped string.
type Unwrapped struct {
TagContent
Body string
}
// TagContent contains the structured contents of a start tag.
type TagContent struct {
Bot string `json:"bot"`
Kind string `json:"kind"`
Meta json.RawMessage `json:"meta,omitempty"`
}
// Parse parses the contents of a wrapped string.
// It returns nil if the string is not wrapped.
func Parse(s string) *Unwrapped {
return parseMatch(isRE.FindStringSubmatch(s))
}
// parseMatch parses the contents of the match (returned from
// [regexp.Regexp.FindStringSubmatch] or similar).
func parseMatch(match []string) *Unwrapped {
// no match
if match == nil {
return nil
}
// bug in regexp ([isRE] or [containsRE])
if len(match) != 3 {
panic(fmt.Sprintf("internal: bug in regexp: found %d match parts, want 3", len(match)))
}
// s is m[0]
tc, body := match[1], match[2]
var tagContent TagContent
if err := json.Unmarshal([]byte(tc), &tagContent); err != nil {
return nil
}
return &Unwrapped{
TagContent: tagContent,
Body: body,
}
}
// ParseAll parses the contents of all wrapped substrings in the
// string. It returns nil if there are no wrapped substrings,
// or any of them are malformed.
func ParseAll(s string) []*Unwrapped {
var us []*Unwrapped
matches := containsRE.FindAllStringSubmatch(s, -1)
if len(matches) == 0 {
return nil
}
for _, m := range matches {
u := parseMatch(m)
if u == nil {
return nil
}
us = append(us, u)
}
return us
}
// Strip removes all wrapped substrings of the string
// and returns the result.
func Strip(s string) string {
return containsRE.ReplaceAllString(s, "")
}
// startTag returns <!-- Generated by Oscar. DO NOT EDIT. {"bot":"$bot","kind":"$kind","meta":$metadata-json} -->
// where $metadata-json is the JSON representaion of the given metadata.
func (w *Wrapper) startTag(metadata any) string {
// DO NOT REMOVE/EDIT this function body.
var js json.RawMessage
if metadata != nil {
js = storage.JSON(metadata)
}
return fmt.Sprintf("<!-- %s %s -->",
tagText,
storage.JSON(TagContent{
Bot: w.bot,
Kind: w.kind,
Meta: js,
}))
}
// DO NOT REMOVE/EDIT.
const endTag = "<!-- oscar-end -->"