blob: 5e7d8c3558ece82da34b76c29b6adf7632ae6759 [file] [log] [blame]
package diffstore
import (
"fmt"
"image"
"math/big"
"path/filepath"
"regexp"
"strings"
"github.com/mattheath/base62"
"go.skia.org/infra/go/fileutil"
"go.skia.org/infra/go/util"
"go.skia.org/infra/golden/go/diff"
"go.skia.org/infra/golden/go/validation"
)
const (
// DIFF_IMG_SEPARATOR is the character that separates two image ids in the
// resulting diff image.
DIFF_IMG_SEPARATOR = "-"
)
// GoldIDPathMapper implements the DiffStoreMapper interface. It translates
// between digests (image ids) and storage paths. It uses diff.DiffMetrics
// as the Gold diff metric.
type GoldDiffStoreMapper struct {
util.LRUCodec
}
// NewGoldDiffStoreMapper returns a new instance of GoldDiffStoreMapper that uses
// a JSON coded to serialize/deserialize instances of diff.DiffMetrics.
func NewGoldDiffStoreMapper(diffInstance interface{}) DiffStoreMapper {
return GoldDiffStoreMapper{LRUCodec: util.JSONCodec(diffInstance)}
}
// DiffFn implements the DiffStoreMapper interface.
func (g GoldDiffStoreMapper) DiffFn(leftImg *image.NRGBA, rightImg *image.NRGBA) (interface{}, *image.NRGBA) {
return diff.DefaultDiffFn(leftImg, rightImg)
}
// DiffID implements the DiffStoreMapper interface.
func (g GoldDiffStoreMapper) DiffID(leftImgID, rightImgID string) string {
_, _, diffID := g.getOrderedDiffID(leftImgID, rightImgID)
return diffID
}
// SplitDiffID implements the DiffStoreMapper interface.
func (g GoldDiffStoreMapper) SplitDiffID(diffID string) (string, string) {
imageIDs := strings.Split(diffID, DIFF_IMG_SEPARATOR)
// TODO(stephana): Remove this legacy handling code as soon as it has converted the
// database in production.
if strings.Contains(diffID, ":") {
imageIDs = strings.Split(diffID, ":")
}
return imageIDs[0], imageIDs[1]
}
// SplitDiffID implements the DiffStoreMapper interface.
func (g GoldDiffStoreMapper) DiffPath(leftImgID, rightImgID string) string {
// Get the diff ID and the left imageID.
leftImgID, _, diffID := g.getOrderedDiffID(leftImgID, rightImgID)
imagePath := fmt.Sprintf("%s.%s", diffID, IMG_EXTENSION)
// For gs images generate a path that corresponds to the GS location.
if strings.HasPrefix(leftImgID, GS_PREFIX) {
dir := LocalDirFromGCSImageID(leftImgID)
return dir + "/" + imagePath
}
return fileutil.TwoLevelRadixPath(imagePath)
}
// ImagePaths implements the DiffStoreMapper interface.
func (g GoldDiffStoreMapper) ImagePaths(imageID string) (string, string, string) {
if strings.HasPrefix(imageID, GS_PREFIX) {
bucket, gsPath := ImageIDToGCSPath(imageID)
localPath := GS_PREFIX + "/" + bucket + "/" + gsPath
return localPath, bucket, gsPath
}
gsPath := fmt.Sprintf("%s.%s", imageID, IMG_EXTENSION)
localPath := fileutil.TwoLevelRadixPath(gsPath)
return localPath, "", gsPath
}
// IsValidDiffImgIDimplements the DiffStoreMapper interface.
func (g GoldDiffStoreMapper) IsValidDiffImgID(diffImgID string) bool {
imageIDs := strings.Split(diffImgID, DIFF_IMG_SEPARATOR)
if len(imageIDs) != 2 {
return false
}
return g.IsValidImgID(imageIDs[0]) && g.IsValidImgID(imageIDs[1])
}
// IsValidImgIDimplements the DiffStoreMapper interface.
func (g GoldDiffStoreMapper) IsValidImgID(imgID string) bool {
if strings.HasPrefix(imgID, GS_PREFIX) {
return ValidGCSImageID(imgID)
}
return validation.IsValidDigest(imgID)
}
func (g GoldDiffStoreMapper) getOrderedDiffID(leftImgID, rightImgID string) (string, string, string) {
leftGS := strings.HasPrefix(leftImgID, GS_PREFIX)
rightGS := strings.HasPrefix(rightImgID, GS_PREFIX)
if leftGS || rightGS {
// Make sure the first imageID is the smaller GS location.
if !leftGS || (rightGS && (rightImgID < leftImgID)) {
leftImgID, rightImgID = rightImgID, leftImgID
}
} else if rightImgID < leftImgID {
// Make sure the smaller digest is left imageID.
leftImgID, rightImgID = rightImgID, leftImgID
}
return leftImgID, rightImgID, leftImgID + DIFF_IMG_SEPARATOR + rightImgID
}
// GCSPathToImageID returns a URL compatible encoding of the given location in
// GCS. It is the inverse of ImageIDToGCSPath.
func GCSPathToImageID(bucket, path string) string {
fullPath := bucket + "/" + strings.TrimSuffix(strings.Trim(path, "/"), "."+IMG_EXTENSION)
return GS_PREFIX + encodeBase62(fullPath)
}
func ImageIDToGCSPath(imageID string) (string, string) {
imageID = decodeBase62(strings.TrimPrefix(imageID, GS_PREFIX))
bucketAndPath := strings.SplitN(imageID, "/", 2)
return bucketAndPath[0], bucketAndPath[1] + "." + IMG_EXTENSION
}
// base62Alphabet is used to verify that a base62 encoded ID only
// contains the valid characters.
var base62Alphabet = regexp.MustCompile("[a-zA-Z0-9]*")
// ValidGCSImageID
func ValidGCSImageID(imageID string) bool {
if !strings.HasPrefix(imageID, GS_PREFIX) || !base62Alphabet.Match([]byte(imageID[len(GS_PREFIX):])) {
return false
}
// Make sure the bucke and path are non-empty.
bucket, path := ImageIDToGCSPath(imageID)
return (bucket != "") && (path != ("." + IMG_EXTENSION))
}
func LocalDirFromGCSImageID(imageID string) string {
bucket, path := ImageIDToGCSPath(imageID)
dir, _ := filepath.Split(path)
return strings.Join([]string{GS_PREFIX, bucket, strings.TrimSuffix(dir, "/")}, "/")
}
//noPad62Encoding allows to base62 encode big.Int's without adding padding.
var noPad62Encoding = base62.NewStdEncoding().Option(base62.Padding(0))
// encodeBase62 encodes the given string to base62 encoding which only contains
// characters and numbers and is URL safe. No padding is added.
func encodeBase62(clearText string) string {
var bigInt big.Int
return noPad62Encoding.EncodeBigInt(bigInt.SetBytes([]byte(clearText)))
}
// decodeBase62 decodes the given base62 encoded string without padding.
func decodeBase62(encStr string) string {
return string(noPad62Encoding.DecodeToBigInt(encStr).Bytes())
}