mirror of
https://github.com/cwinfo/matterbridge.git
synced 2024-11-10 14:30:26 +00:00
724 lines
17 KiB
Go
724 lines
17 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/base32"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/mail"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
goi18n "github.com/mattermost/go-i18n/i18n"
|
|
"github.com/pborman/uuid"
|
|
)
|
|
|
|
const (
|
|
LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz"
|
|
UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
NUMBERS = "0123456789"
|
|
SYMBOLS = " !\"\\#$%&'()*+,-./:;<=>?@[]^_`|~"
|
|
)
|
|
|
|
type StringInterface map[string]interface{}
|
|
type StringMap map[string]string
|
|
type StringArray []string
|
|
|
|
func (sa StringArray) Remove(input string) StringArray {
|
|
for index := range sa {
|
|
if sa[index] == input {
|
|
ret := make(StringArray, 0, len(sa)-1)
|
|
ret = append(ret, sa[:index]...)
|
|
return append(ret, sa[index+1:]...)
|
|
}
|
|
}
|
|
return sa
|
|
}
|
|
|
|
func (sa StringArray) Contains(input string) bool {
|
|
for index := range sa {
|
|
if sa[index] == input {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
func (sa StringArray) Equals(input StringArray) bool {
|
|
|
|
if len(sa) != len(input) {
|
|
return false
|
|
}
|
|
|
|
for index := range sa {
|
|
|
|
if sa[index] != input[index] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
var translateFunc goi18n.TranslateFunc = nil
|
|
|
|
func AppErrorInit(t goi18n.TranslateFunc) {
|
|
translateFunc = t
|
|
}
|
|
|
|
type AppError struct {
|
|
Id string `json:"id"`
|
|
Message string `json:"message"` // Message to be display to the end user without debugging information
|
|
DetailedError string `json:"detailed_error"` // Internal error string to help the developer
|
|
RequestId string `json:"request_id,omitempty"` // The RequestId that's also set in the header
|
|
StatusCode int `json:"status_code,omitempty"` // The http status code
|
|
Where string `json:"-"` // The function where it happened in the form of Struct.Func
|
|
IsOAuth bool `json:"is_oauth,omitempty"` // Whether the error is OAuth specific
|
|
params map[string]interface{}
|
|
}
|
|
|
|
func (er *AppError) Error() string {
|
|
return er.Where + ": " + er.Message + ", " + er.DetailedError
|
|
}
|
|
|
|
func (er *AppError) Translate(T goi18n.TranslateFunc) {
|
|
if T == nil {
|
|
er.Message = er.Id
|
|
return
|
|
}
|
|
|
|
if er.params == nil {
|
|
er.Message = T(er.Id)
|
|
} else {
|
|
er.Message = T(er.Id, er.params)
|
|
}
|
|
}
|
|
|
|
func (er *AppError) SystemMessage(T goi18n.TranslateFunc) string {
|
|
if er.params == nil {
|
|
return T(er.Id)
|
|
} else {
|
|
return T(er.Id, er.params)
|
|
}
|
|
}
|
|
|
|
func (er *AppError) ToJson() string {
|
|
b, _ := json.Marshal(er)
|
|
return string(b)
|
|
}
|
|
|
|
// AppErrorFromJson will decode the input and return an AppError
|
|
func AppErrorFromJson(data io.Reader) *AppError {
|
|
str := ""
|
|
bytes, rerr := ioutil.ReadAll(data)
|
|
if rerr != nil {
|
|
str = rerr.Error()
|
|
} else {
|
|
str = string(bytes)
|
|
}
|
|
|
|
decoder := json.NewDecoder(strings.NewReader(str))
|
|
var er AppError
|
|
err := decoder.Decode(&er)
|
|
if err == nil {
|
|
return &er
|
|
} else {
|
|
return NewAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, "body: "+str, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func NewAppError(where string, id string, params map[string]interface{}, details string, status int) *AppError {
|
|
ap := &AppError{}
|
|
ap.Id = id
|
|
ap.params = params
|
|
ap.Message = id
|
|
ap.Where = where
|
|
ap.DetailedError = details
|
|
ap.StatusCode = status
|
|
ap.IsOAuth = false
|
|
ap.Translate(translateFunc)
|
|
return ap
|
|
}
|
|
|
|
var encoding = base32.NewEncoding("ybndrfg8ejkmcpqxot1uwisza345h769")
|
|
|
|
// NewId is a globally unique identifier. It is a [A-Z0-9] string 26
|
|
// characters long. It is a UUID version 4 Guid that is zbased32 encoded
|
|
// with the padding stripped off.
|
|
func NewId() string {
|
|
var b bytes.Buffer
|
|
encoder := base32.NewEncoder(encoding, &b)
|
|
encoder.Write(uuid.NewRandom())
|
|
encoder.Close()
|
|
b.Truncate(26) // removes the '==' padding
|
|
return b.String()
|
|
}
|
|
|
|
// NewRandomTeamName is a NewId that will be a valid team name.
|
|
func NewRandomTeamName() string {
|
|
teamName := NewId()
|
|
for IsReservedTeamName(teamName) {
|
|
teamName = NewId()
|
|
}
|
|
return teamName
|
|
}
|
|
|
|
// NewRandomString returns a random string of the given length.
|
|
// The resulting entropy will be (5 * length) bits.
|
|
func NewRandomString(length int) string {
|
|
data := make([]byte, 1+(length*5/8))
|
|
rand.Read(data)
|
|
return encoding.EncodeToString(data)[:length]
|
|
}
|
|
|
|
// NewRandomBase32String returns a base32 encoded string of a random slice
|
|
// of bytes of the given size. The resulting entropy will be (8 * size) bits.
|
|
func NewRandomBase32String(size int) string {
|
|
data := make([]byte, size)
|
|
rand.Read(data)
|
|
return base32.StdEncoding.EncodeToString(data)
|
|
}
|
|
|
|
// GetMillis is a convenience method to get milliseconds since epoch.
|
|
func GetMillis() int64 {
|
|
return time.Now().UnixNano() / int64(time.Millisecond)
|
|
}
|
|
|
|
// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
|
|
func GetMillisForTime(thisTime time.Time) int64 {
|
|
return thisTime.UnixNano() / int64(time.Millisecond)
|
|
}
|
|
|
|
// PadDateStringZeros is a convenience method to pad 2 digit date parts with zeros to meet ISO 8601 format
|
|
func PadDateStringZeros(dateString string) string {
|
|
parts := strings.Split(dateString, "-")
|
|
for index, part := range parts {
|
|
if len(part) == 1 {
|
|
parts[index] = "0" + part
|
|
}
|
|
}
|
|
dateString = strings.Join(parts[:], "-")
|
|
return dateString
|
|
}
|
|
|
|
// GetStartOfDayMillis is a convenience method to get milliseconds since epoch for provided date's start of day
|
|
func GetStartOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 {
|
|
localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset)
|
|
resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 0, 0, 0, 0, localSearchTimeZone)
|
|
return GetMillisForTime(resultTime)
|
|
}
|
|
|
|
// GetEndOfDayMillis is a convenience method to get milliseconds since epoch for provided date's end of day
|
|
func GetEndOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 {
|
|
localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset)
|
|
resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 23, 59, 59, 999999999, localSearchTimeZone)
|
|
return GetMillisForTime(resultTime)
|
|
}
|
|
|
|
func CopyStringMap(originalMap map[string]string) map[string]string {
|
|
copyMap := make(map[string]string)
|
|
for k, v := range originalMap {
|
|
copyMap[k] = v
|
|
}
|
|
return copyMap
|
|
}
|
|
|
|
// MapToJson converts a map to a json string
|
|
func MapToJson(objmap map[string]string) string {
|
|
b, _ := json.Marshal(objmap)
|
|
return string(b)
|
|
}
|
|
|
|
// MapBoolToJson converts a map to a json string
|
|
func MapBoolToJson(objmap map[string]bool) string {
|
|
b, _ := json.Marshal(objmap)
|
|
return string(b)
|
|
}
|
|
|
|
// MapFromJson will decode the key/value pair map
|
|
func MapFromJson(data io.Reader) map[string]string {
|
|
decoder := json.NewDecoder(data)
|
|
|
|
var objmap map[string]string
|
|
if err := decoder.Decode(&objmap); err != nil {
|
|
return make(map[string]string)
|
|
} else {
|
|
return objmap
|
|
}
|
|
}
|
|
|
|
// MapFromJson will decode the key/value pair map
|
|
func MapBoolFromJson(data io.Reader) map[string]bool {
|
|
decoder := json.NewDecoder(data)
|
|
|
|
var objmap map[string]bool
|
|
if err := decoder.Decode(&objmap); err != nil {
|
|
return make(map[string]bool)
|
|
} else {
|
|
return objmap
|
|
}
|
|
}
|
|
|
|
func ArrayToJson(objmap []string) string {
|
|
b, _ := json.Marshal(objmap)
|
|
return string(b)
|
|
}
|
|
|
|
func ArrayFromJson(data io.Reader) []string {
|
|
decoder := json.NewDecoder(data)
|
|
|
|
var objmap []string
|
|
if err := decoder.Decode(&objmap); err != nil {
|
|
return make([]string, 0)
|
|
} else {
|
|
return objmap
|
|
}
|
|
}
|
|
|
|
func ArrayFromInterface(data interface{}) []string {
|
|
stringArray := []string{}
|
|
|
|
dataArray, ok := data.([]interface{})
|
|
if !ok {
|
|
return stringArray
|
|
}
|
|
|
|
for _, v := range dataArray {
|
|
if str, ok := v.(string); ok {
|
|
stringArray = append(stringArray, str)
|
|
}
|
|
}
|
|
|
|
return stringArray
|
|
}
|
|
|
|
func StringInterfaceToJson(objmap map[string]interface{}) string {
|
|
b, _ := json.Marshal(objmap)
|
|
return string(b)
|
|
}
|
|
|
|
func StringInterfaceFromJson(data io.Reader) map[string]interface{} {
|
|
decoder := json.NewDecoder(data)
|
|
|
|
var objmap map[string]interface{}
|
|
if err := decoder.Decode(&objmap); err != nil {
|
|
return make(map[string]interface{})
|
|
} else {
|
|
return objmap
|
|
}
|
|
}
|
|
|
|
func StringToJson(s string) string {
|
|
b, _ := json.Marshal(s)
|
|
return string(b)
|
|
}
|
|
|
|
func StringFromJson(data io.Reader) string {
|
|
decoder := json.NewDecoder(data)
|
|
|
|
var s string
|
|
if err := decoder.Decode(&s); err != nil {
|
|
return ""
|
|
} else {
|
|
return s
|
|
}
|
|
}
|
|
|
|
func GetServerIpAddress(iface string) string {
|
|
var addrs []net.Addr
|
|
if len(iface) == 0 {
|
|
var err error
|
|
addrs, err = net.InterfaceAddrs()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
} else {
|
|
interfaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, i := range interfaces {
|
|
if i.Name == iface {
|
|
addrs, err = i.Addrs()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
|
|
if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() && !ip.IP.IsLinkLocalMulticast() {
|
|
if ip.IP.To4() != nil {
|
|
return ip.IP.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func IsLower(s string) bool {
|
|
return strings.ToLower(s) == s
|
|
}
|
|
|
|
func IsValidEmail(email string) bool {
|
|
if !IsLower(email) {
|
|
return false
|
|
}
|
|
|
|
if addr, err := mail.ParseAddress(email); err != nil {
|
|
return false
|
|
} else if addr.Name != "" {
|
|
// mail.ParseAddress accepts input of the form "Billy Bob <billy@example.com>" which we don't allow
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
var reservedName = []string{
|
|
"admin",
|
|
"api",
|
|
"channel",
|
|
"claim",
|
|
"error",
|
|
"help",
|
|
"landing",
|
|
"login",
|
|
"mfa",
|
|
"oauth",
|
|
"plug",
|
|
"plugins",
|
|
"post",
|
|
"signup",
|
|
}
|
|
|
|
func IsValidChannelIdentifier(s string) bool {
|
|
|
|
if !IsValidAlphaNumHyphenUnderscore(s, true) {
|
|
return false
|
|
}
|
|
|
|
if len(s) < CHANNEL_NAME_MIN_LENGTH {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func IsValidAlphaNum(s string) bool {
|
|
validAlphaNum := regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`)
|
|
|
|
return validAlphaNum.MatchString(s)
|
|
}
|
|
|
|
func IsValidAlphaNumHyphenUnderscore(s string, withFormat bool) bool {
|
|
if withFormat {
|
|
validAlphaNumHyphenUnderscore := regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`)
|
|
return validAlphaNumHyphenUnderscore.MatchString(s)
|
|
}
|
|
|
|
validSimpleAlphaNumHyphenUnderscore := regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`)
|
|
return validSimpleAlphaNumHyphenUnderscore.MatchString(s)
|
|
}
|
|
|
|
func Etag(parts ...interface{}) string {
|
|
|
|
etag := CurrentVersion
|
|
|
|
for _, part := range parts {
|
|
etag += fmt.Sprintf(".%v", part)
|
|
}
|
|
|
|
return etag
|
|
}
|
|
|
|
var validHashtag = regexp.MustCompile(`^(#\pL[\pL\d\-_.]*[\pL\d])$`)
|
|
var puncStart = regexp.MustCompile(`^[^\pL\d\s#]+`)
|
|
var hashtagStart = regexp.MustCompile(`^#{2,}`)
|
|
var puncEnd = regexp.MustCompile(`[^\pL\d\s]+$`)
|
|
|
|
func ParseHashtags(text string) (string, string) {
|
|
words := strings.Fields(text)
|
|
|
|
hashtagString := ""
|
|
plainString := ""
|
|
for _, word := range words {
|
|
// trim off surrounding punctuation
|
|
word = puncStart.ReplaceAllString(word, "")
|
|
word = puncEnd.ReplaceAllString(word, "")
|
|
|
|
// and remove extra pound #s
|
|
word = hashtagStart.ReplaceAllString(word, "#")
|
|
|
|
if validHashtag.MatchString(word) {
|
|
hashtagString += " " + word
|
|
} else {
|
|
plainString += " " + word
|
|
}
|
|
}
|
|
|
|
if len(hashtagString) > 1000 {
|
|
hashtagString = hashtagString[:999]
|
|
lastSpace := strings.LastIndex(hashtagString, " ")
|
|
if lastSpace > -1 {
|
|
hashtagString = hashtagString[:lastSpace]
|
|
} else {
|
|
hashtagString = ""
|
|
}
|
|
}
|
|
|
|
return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString)
|
|
}
|
|
|
|
func IsFileExtImage(ext string) bool {
|
|
ext = strings.ToLower(ext)
|
|
for _, imgExt := range IMAGE_EXTENSIONS {
|
|
if ext == imgExt {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func GetImageMimeType(ext string) string {
|
|
ext = strings.ToLower(ext)
|
|
if len(IMAGE_MIME_TYPES[ext]) == 0 {
|
|
return "image"
|
|
} else {
|
|
return IMAGE_MIME_TYPES[ext]
|
|
}
|
|
}
|
|
|
|
func ClearMentionTags(post string) string {
|
|
post = strings.Replace(post, "<mention>", "", -1)
|
|
post = strings.Replace(post, "</mention>", "", -1)
|
|
return post
|
|
}
|
|
|
|
func IsValidHttpUrl(rawUrl string) bool {
|
|
if strings.Index(rawUrl, "http://") != 0 && strings.Index(rawUrl, "https://") != 0 {
|
|
return false
|
|
}
|
|
|
|
if _, err := url.ParseRequestURI(rawUrl); err != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func IsValidTurnOrStunServer(rawUri string) bool {
|
|
if strings.Index(rawUri, "turn:") != 0 && strings.Index(rawUri, "stun:") != 0 {
|
|
return false
|
|
}
|
|
|
|
if _, err := url.ParseRequestURI(rawUri); err != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func IsSafeLink(link *string) bool {
|
|
if link != nil {
|
|
if IsValidHttpUrl(*link) {
|
|
return true
|
|
} else if strings.HasPrefix(*link, "/") {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func IsValidWebsocketUrl(rawUrl string) bool {
|
|
if strings.Index(rawUrl, "ws://") != 0 && strings.Index(rawUrl, "wss://") != 0 {
|
|
return false
|
|
}
|
|
|
|
if _, err := url.ParseRequestURI(rawUrl); err != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func IsValidTrueOrFalseString(value string) bool {
|
|
return value == "true" || value == "false"
|
|
}
|
|
|
|
func IsValidNumberString(value string) bool {
|
|
if _, err := strconv.Atoi(value); err != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func IsValidId(value string) bool {
|
|
if len(value) != 26 {
|
|
return false
|
|
}
|
|
|
|
for _, r := range value {
|
|
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Copied from https://golang.org/src/net/dnsclient.go#L119
|
|
func IsDomainName(s string) bool {
|
|
// See RFC 1035, RFC 3696.
|
|
// Presentation format has dots before every label except the first, and the
|
|
// terminal empty label is optional here because we assume fully-qualified
|
|
// (absolute) input. We must therefore reserve space for the first and last
|
|
// labels' length octets in wire format, where they are necessary and the
|
|
// maximum total length is 255.
|
|
// So our _effective_ maximum is 253, but 254 is not rejected if the last
|
|
// character is a dot.
|
|
l := len(s)
|
|
if l == 0 || l > 254 || l == 254 && s[l-1] != '.' {
|
|
return false
|
|
}
|
|
|
|
last := byte('.')
|
|
ok := false // Ok once we've seen a letter.
|
|
partlen := 0
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
switch {
|
|
default:
|
|
return false
|
|
case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_':
|
|
ok = true
|
|
partlen++
|
|
case '0' <= c && c <= '9':
|
|
// fine
|
|
partlen++
|
|
case c == '-':
|
|
// Byte before dash cannot be dot.
|
|
if last == '.' {
|
|
return false
|
|
}
|
|
partlen++
|
|
case c == '.':
|
|
// Byte before dot cannot be dot, dash.
|
|
if last == '.' || last == '-' {
|
|
return false
|
|
}
|
|
if partlen > 63 || partlen == 0 {
|
|
return false
|
|
}
|
|
partlen = 0
|
|
}
|
|
last = c
|
|
}
|
|
if last == '-' || partlen > 63 {
|
|
return false
|
|
}
|
|
|
|
return ok
|
|
}
|
|
|
|
func RemoveDuplicateStrings(in []string) []string {
|
|
out := []string{}
|
|
seen := make(map[string]bool, len(in))
|
|
|
|
for _, item := range in {
|
|
if !seen[item] {
|
|
out = append(out, item)
|
|
|
|
seen[item] = true
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func GetPreferredTimezone(timezone StringMap) string {
|
|
if timezone["useAutomaticTimezone"] == "true" {
|
|
return timezone["automaticTimezone"]
|
|
}
|
|
|
|
return timezone["manualTimezone"]
|
|
}
|
|
|
|
// IsSamlFile checks if filename is a SAML file.
|
|
func IsSamlFile(saml *SamlSettings, filename string) bool {
|
|
return filename == *saml.PublicCertificateFile || filename == *saml.PrivateKeyFile || filename == *saml.IdpCertificateFile
|
|
}
|
|
|
|
func AsStringBoolMap(list []string) map[string]bool {
|
|
listMap := map[string]bool{}
|
|
for _, p := range list {
|
|
listMap[p] = true
|
|
}
|
|
return listMap
|
|
}
|
|
|
|
// SanitizeUnicode will remove undesirable Unicode characters from a string.
|
|
func SanitizeUnicode(s string) string {
|
|
return strings.Map(filterBlocklist, s)
|
|
}
|
|
|
|
// filterBlocklist returns `r` if it is not in the blocklist, otherwise drop (-1).
|
|
// Blocklist is taken from https://www.w3.org/TR/unicode-xml/#Charlist
|
|
func filterBlocklist(r rune) rune {
|
|
const drop = -1
|
|
switch r {
|
|
case '\u0340', '\u0341': // clones of grave and acute; deprecated in Unicode
|
|
return drop
|
|
case '\u17A3', '\u17D3': // obsolete characters for Khmer; deprecated in Unicode
|
|
return drop
|
|
case '\u2028', '\u2029': // line and paragraph separator
|
|
return drop
|
|
case '\u202A', '\u202B', '\u202C', '\u202D', '\u202E': // BIDI embedding controls
|
|
return drop
|
|
case '\u206A', '\u206B': // activate/inhibit symmetric swapping; deprecated in Unicode
|
|
return drop
|
|
case '\u206C', '\u206D': // activate/inhibit Arabic form shaping; deprecated in Unicode
|
|
return drop
|
|
case '\u206E', '\u206F': // activate/inhibit national digit shapes; deprecated in Unicode
|
|
return drop
|
|
case '\uFFF9', '\uFFFA', '\uFFFB': // interlinear annotation characters
|
|
return drop
|
|
case '\uFEFF': // byte order mark
|
|
return drop
|
|
case '\uFFFC': // object replacement character
|
|
return drop
|
|
}
|
|
|
|
// Scoping for musical notation
|
|
if r >= 0x0001D173 && r <= 0x0001D17A {
|
|
return drop
|
|
}
|
|
|
|
// Language tag code points
|
|
if r >= 0x000E0000 && r <= 0x000E007F {
|
|
return drop
|
|
}
|
|
|
|
return r
|
|
}
|