package str_utils

import "unicode/utf8"

// ForceValidUTF8 returns a string guaranteed to be valid UTF-8 even if the
// input isn't, by replacing any invalid bytes with a valid UTF-8 encoding of
// the Unicode Replacement Character (\uFFFD).
//
// The protobuf serialization library will reject invalid UTF-8 with an
// unhelpful error message:
//
//	string field contains invalid UTF-8
//
// Passing a string result through this function makes invalid UTF-8 instead
// emerge as placeholder characters on the other side of the wire protocol,
// giving a better chance of still returning a partially-legible message
// instead of a generic character encoding error.
//
// This is intended for user-facing messages such as diagnostic summary and
// detail messages, where Terraform will just treat the value as opaque and
// it's ultimately up to the user and their terminal or web browser to
// interpret the result. Don't use this for strings that have machine-readable
// meaning.
//
// ref: https://github.com/hashicorp/terraform-plugin-go/pull/237/files#diff-93b39f6117fa01617b2055cd35d7bd91fcab9f5dadccfe2a342ee096d57b16d9L1
func ForceValidUTF8(s string) string {
	// Most strings that pass through here will already be valid UTF-8 and
	// utf8.ValidString has a fast path which will beat our rune-by-rune
	// analysis below, so it's worth the cost of walking the string twice
	// in the rarer invalid case.
	if utf8.ValidString(s) {
		return s
	}

	// If we get down here then we know there's at least one invalid UTF-8
	// sequence in the string, so in this slow path we'll reconstruct the
	// string one rune at a time, guaranteeing that we'll only write valid
	// UTF-8 sequences into the resulting buffer.
	//
	// Any invalid string will grow at least a little larger as a result of
	// this operation so we expand it.
	ret := make([]byte, 0, 2*len(s))
	for {
		// If the first byte in s is not the start of a valid UTF-8 sequence
		// then the following will return utf8.RuneError, 1, where
		// utf8.RuneError is the unicode replacement character.
		r, advance := utf8.DecodeRuneInString(s)
		if advance == 0 {
			break
		}
		s = s[advance:]
		ret = utf8.AppendRune(ret, r)
	}
	return string(ret)
}
