diff --git a/hash.go b/hash.go index 673553c..5eefec3 100644 --- a/hash.go +++ b/hash.go @@ -151,6 +151,21 @@ var ( nameBase64 = base64.NewEncoding(nameCharset) ) +// These funcs mimic the unicode package API, but byte-based since we know +// base64 is all ASCII. + +func isDigit(b byte) bool { return '0' <= b && b <= '9' } +func isLower(b byte) bool { return 'a' <= b && b <= 'z' } +func isUpper(b byte) bool { return 'A' <= b && b <= 'Z' } +func toLower(b byte) byte { return b + ('a' - 'A') } +func toUpper(b byte) byte { return b - ('a' - 'A') } + +// hashWith returns a hashed version of name, including the provided salt as well as +// opts.Seed into the hash input. +// +// The result is always four bytes long. If the input was a valid identifier, +// the output remains equally exported or unexported. Note that this process is +// reproducible, but not reversible. func hashWith(salt []byte, name string) string { if len(salt) == 0 { panic("hashWith: empty salt") @@ -164,12 +179,35 @@ func hashWith(salt []byte, name string) string { d.Write(salt) d.Write(opts.Seed) io.WriteString(d, name) - sum := nameBase64.EncodeToString(d.Sum(nil)) - - // TODO: Just make the first letter uppercase or lowercase as needed. - // This is also not needed for non-names, like import paths. + sum := make([]byte, nameBase64.EncodedLen(d.Size())) + nameBase64.Encode(sum, d.Sum(nil)) + sum = sum[:length] + + // Even if we are hashing a package path, we still want the result to be + // a valid identifier, since we'll use it as the package name too. + if isDigit(sum[0]) { + // Turn "3foo" into "Dfoo". + // Similar to toLower, since uppercase letters go after digits + // in the ASCII table. + sum[0] += 'A' - '0' + } + // Keep the result equally exported or not, if it was an identifier. + if !token.IsIdentifier(name) { + return string(sum) + } if token.IsExported(name) { - return "Z" + sum[:length] + if sum[0] == '_' { + // Turn "_foo" into "Zfoo". + sum[0] = 'Z' + } else if isLower(sum[0]) { + // Turn "afoo" into "Afoo". + sum[0] = toUpper(sum[0]) + } + } else { + if isUpper(sum[0]) { + // Turn "Afoo" into "afoo". + sum[0] = toLower(sum[0]) + } } - return "z" + sum[:length] + return string(sum) }