Files
2026-07-13 12:32:25 +08:00

168 lines
5.6 KiB
Go

// Copyright 2025 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import "math/rand/v2"
// replacements contains all word replacements, to ensure that similar sequences throughout the file are replaced with
// the same values. This allows for foreign keys to continue functioning.
var replacements = make(map[string]string)
// createdReplacements contains all replacement strings that have been created, in an effort to prevent unintended
// duplicates (which can cause primary/unique keys to unexpectedly fail).
var createdReplacements = make(map[string]struct{})
// replacementCharacters contains all of the characters that are valid substitutions for replacement.
var replacementCharacters = []rune{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}
// replacementCharactersLimited contains all of the characters that are valid substitutions in a limited context (such as hex-encoded strings).
var replacementCharactersLimited = []rune{'a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
// getReplacement returns the replacement of the given string if applicable.
func getReplacement(sequence []rune, useLimited bool) string {
original := string(sequence)
if len(sequence) == 0 || isSpecialRune(sequence[0]) || isReserved(original) {
return original
}
replacement, ok := replacements[original]
if ok {
return replacement
}
allNumbers := true
for i := 0; i < len(sequence); i++ {
if isNumber(sequence[i]) { // We don't replace numbers
continue
} else {
allNumbers = false
if useLimited {
sequence[i] = replacementCharactersLimited[rand.Uint32()%uint32(len(replacementCharactersLimited))]
} else {
sequence[i] = replacementCharacters[rand.Uint32()%uint32(len(replacementCharacters))]
}
}
}
if allNumbers {
return original
}
newStr := string(sequence)
if _, ok = createdReplacements[newStr]; (ok || isReserved(newStr)) && !useLimited {
// If we've somehow randomly created a reserved or duplicate word, then we recursively try again
return getReplacement([]rune(original), useLimited)
}
replacements[original] = newStr
createdReplacements[newStr] = struct{}{}
return newStr
}
// handleUUIDs exists as UUIDs must use a limited set of replacement runes since not all runes are valid.
func handleUUIDs(sequences [][]rune) {
for i := range sequences {
// Ensure that there are enough sequences
if i+8 >= len(sequences) {
continue
}
// Check that the sequence lengths are valid
if len(sequences[i]) != 8 ||
len(sequences[i+1]) != 1 ||
len(sequences[i+2]) != 4 ||
len(sequences[i+3]) != 1 ||
len(sequences[i+4]) != 4 ||
len(sequences[i+5]) != 1 ||
len(sequences[i+6]) != 4 ||
len(sequences[i+7]) != 1 ||
len(sequences[i+8]) != 12 {
continue
}
// Check for the dashes that separate UUIDs
if sequences[i+1][0] != '-' ||
sequences[i+3][0] != '-' ||
sequences[i+5][0] != '-' ||
sequences[i+7][0] != '-' {
continue
}
// Check that the characters are all alphanumeric
if !matches(sequences[i], isAlphanumeric) ||
!matches(sequences[i+2], isAlphanumeric) ||
!matches(sequences[i+4], isAlphanumeric) ||
!matches(sequences[i+6], isAlphanumeric) ||
!matches(sequences[i+8], isAlphanumeric) {
continue
}
var newStr string
for seqIdx, seq := range [][]rune{sequences[i], sequences[i+2], sequences[i+4], sequences[i+6], sequences[i+8]} {
if seqIdx > 0 {
newStr += "-"
}
newStr += getReplacement(seq, true)
}
// Set the UUID as a replacement
sequences[i] = []rune(newStr)
sequences[i+1] = nil
sequences[i+2] = nil
sequences[i+3] = nil
sequences[i+4] = nil
sequences[i+5] = nil
sequences[i+6] = nil
sequences[i+7] = nil
sequences[i+8] = nil
replacements[newStr] = newStr
}
}
// isSpecialRune returns whether the given rune is a control character or symbol (excluding underscores).
func isSpecialRune(r rune) bool {
return (r >= 0 && r <= '/') || (r >= ':' && r <= '@') || (r >= '[' && r <= '^') || r == '`' || (r >= '{' && r <= 191)
}
// isNumber returns whether the given rune is a number.
func isNumber(r rune) bool {
return r >= '0' && r <= '9'
}
// isCharacter returns whether the given rune is a character.
func isCharacter(r rune) bool {
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z')
}
// isAlphanumeric returns whether the given rune is a numeber or character.
func isAlphanumeric(r rune) bool {
return isCharacter(r) || isNumber(r)
}
// isControlCharacter returns whether the current runeCache and the given nextRune creates a control character (such as
// the null character \N).
func isControlCharacter(runeCache []rune, nextRune rune) bool {
if len(runeCache) == 0 {
return false
}
if runeCache[len(runeCache)-1] == '\\' {
switch nextRune {
case 'b', 'f', 'n', 'r', 't', 'v', 'x', 'N':
return true
}
}
return false
}
// matches returns whether each rune in the sequence returns true when passed to the given function.
func matches(sequence []rune, f func(rune) bool) bool {
for _, r := range sequence {
if !f(r) {
return false
}
}
return true
}