mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-20 14:53:42 +08:00
239 lines
6.8 KiB
Go
239 lines
6.8 KiB
Go
package device
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// The offline table is generated by scripts/update-carriers.py from Android's
|
|
// versioned carrier ID database, with the previous global table retained as a
|
|
// fallback for PLMNs that Android does not yet catalogue.
|
|
//
|
|
//go:embed mccmnc.json
|
|
var carrierDatabaseJSON []byte
|
|
|
|
type carrierDatabase struct {
|
|
Carriers map[string][]string `json:"c"`
|
|
Countries map[string]string `json:"i"`
|
|
Rules []carrierRule `json:"r"`
|
|
}
|
|
|
|
type carrierRule struct {
|
|
Name string `json:"n"`
|
|
PLMNs []string `json:"m"`
|
|
IMSIPatterns []string `json:"x"`
|
|
SPNs []string `json:"s"`
|
|
GID1Prefixes []string `json:"g1"`
|
|
GID2Prefixes []string `json:"g2"`
|
|
ICCIDPrefixes []string `json:"i"`
|
|
}
|
|
|
|
// CarrierIdentity contains the SIM-issued values used by Android's carrier
|
|
// resolver. MNC length comes from EF_AD; GID values come from EF_GID1/2.
|
|
type CarrierIdentity struct {
|
|
IMSI string
|
|
ICCID string
|
|
SPN string
|
|
GID1 string
|
|
GID2 string
|
|
MNCLength int
|
|
}
|
|
|
|
// CountryForMCC returns the ISO alpha-2 country/territory code associated with
|
|
// a three-digit mobile country code in the embedded Android carrier database.
|
|
func CountryForMCC(mcc string) (string, bool) {
|
|
mcc = strings.TrimSpace(mcc)
|
|
if len(mcc) != 3 {
|
|
return "", false
|
|
}
|
|
country := strings.ToUpper(strings.TrimSpace(globalCarrierDatabase.Countries[mcc]))
|
|
return country, len(country) == 2
|
|
}
|
|
|
|
// MCCsByCountry returns the complete MCC grouping from the embedded carrier
|
|
// database, keyed by ISO alpha-2 country/territory code. The returned map and
|
|
// slices are new values and may be safely modified by callers.
|
|
func MCCsByCountry() map[string][]string {
|
|
result := make(map[string][]string)
|
|
for mcc, rawCountry := range globalCarrierDatabase.Countries {
|
|
country := strings.ToUpper(strings.TrimSpace(rawCountry))
|
|
mcc = strings.TrimSpace(mcc)
|
|
if len(country) != 2 || len(mcc) != 3 {
|
|
continue
|
|
}
|
|
result[country] = append(result[country], mcc)
|
|
}
|
|
for country := range result {
|
|
sort.Strings(result[country])
|
|
}
|
|
return result
|
|
}
|
|
|
|
var globalCarrierDatabase = func() carrierDatabase {
|
|
var database carrierDatabase
|
|
if err := json.Unmarshal(carrierDatabaseJSON, &database); err != nil {
|
|
panic("device: invalid embedded MCC/MNC database: " + err.Error())
|
|
}
|
|
return database
|
|
}()
|
|
|
|
// CarrierForPLMN returns the offline carrier display name and ISO alpha-2
|
|
// country/territory code for a numeric five- or six-digit PLMN.
|
|
func CarrierForPLMN(plmn string) (name, countryCode string, ok bool) {
|
|
plmn = strings.TrimSpace(plmn)
|
|
if !decimalDigits(plmn, 5, 6) {
|
|
return "", "", false
|
|
}
|
|
entry, ok := globalCarrierDatabase.Carriers[plmn]
|
|
if !ok || len(entry) == 0 || strings.TrimSpace(entry[0]) == "" {
|
|
return "", "", false
|
|
}
|
|
name = strings.TrimSpace(entry[0])
|
|
if len(entry) > 1 {
|
|
countryCode = strings.ToUpper(strings.TrimSpace(entry[1]))
|
|
}
|
|
return name, countryCode, true
|
|
}
|
|
|
|
// CarrierForIMSI resolves the home PLMN carried by an IMSI. MNCs may contain
|
|
// either two or three digits, so prefer an exact six-digit database match and
|
|
// then fall back to the five-digit form. This avoids treating the first three
|
|
// subscriber digits as a three-digit MNC for networks such as 234-33.
|
|
func CarrierForIMSI(imsi string) (plmn, name, countryCode string, ok bool) {
|
|
imsi = strings.TrimSpace(imsi)
|
|
if !decimalDigits(imsi, 5, 20) {
|
|
return "", "", "", false
|
|
}
|
|
for _, length := range []int{6, 5} {
|
|
if len(imsi) < length {
|
|
continue
|
|
}
|
|
candidate := imsi[:length]
|
|
carrier, country, found := CarrierForPLMN(candidate)
|
|
if found {
|
|
return candidate, carrier, country, true
|
|
}
|
|
}
|
|
return "", "", "", false
|
|
}
|
|
|
|
// CarrierForSIM applies the constrained Android carrier-ID rules before the
|
|
// MCC/MNC fallback. This is important for MVNO and travel eSIM profiles where
|
|
// several customer-facing carriers authenticate through the same home PLMN.
|
|
func CarrierForSIM(identity CarrierIdentity) (plmn, name, countryCode string, ok bool) {
|
|
imsi := strings.TrimSpace(identity.IMSI)
|
|
if !decimalDigits(imsi, 5, 20) || IsPlaceholderIMSI(imsi) {
|
|
return "", "", "", false
|
|
}
|
|
plmns := carrierPLMNCandidates(imsi, identity.MNCLength)
|
|
bestScore := -1
|
|
for _, rule := range globalCarrierDatabase.Rules {
|
|
matchedPLMN := firstMatchingValue(rule.PLMNs, func(value string) bool {
|
|
return containsString(plmns, value)
|
|
})
|
|
if matchedPLMN == "" {
|
|
continue
|
|
}
|
|
score := 1 << 8
|
|
if len(rule.IMSIPatterns) > 0 {
|
|
if firstMatchingValue(rule.IMSIPatterns, func(pattern string) bool { return imsiPatternMatch(imsi, pattern) }) == "" {
|
|
continue
|
|
}
|
|
score += 1 << 7
|
|
}
|
|
if len(rule.ICCIDPrefixes) > 0 {
|
|
if firstMatchingValue(rule.ICCIDPrefixes, func(prefix string) bool { return strings.HasPrefix(identity.ICCID, prefix) }) == "" {
|
|
continue
|
|
}
|
|
score += 1 << 6
|
|
}
|
|
if len(rule.GID1Prefixes) > 0 {
|
|
if firstMatchingValue(rule.GID1Prefixes, func(prefix string) bool { return prefixFold(identity.GID1, prefix) }) == "" {
|
|
continue
|
|
}
|
|
score += 1 << 5
|
|
}
|
|
if len(rule.GID2Prefixes) > 0 {
|
|
if firstMatchingValue(rule.GID2Prefixes, func(prefix string) bool { return prefixFold(identity.GID2, prefix) }) == "" {
|
|
continue
|
|
}
|
|
score += 1 << 4
|
|
}
|
|
if len(rule.SPNs) > 0 {
|
|
if !containsFold(rule.SPNs, identity.SPN) {
|
|
continue
|
|
}
|
|
score += 1 << 1
|
|
}
|
|
if score > bestScore {
|
|
bestScore = score
|
|
plmn = matchedPLMN
|
|
name = strings.TrimSpace(rule.Name)
|
|
}
|
|
}
|
|
if bestScore >= 0 && name != "" {
|
|
countryCode, _ = CountryForMCC(plmn[:3])
|
|
return plmn, name, countryCode, true
|
|
}
|
|
return CarrierForIMSI(imsi)
|
|
}
|
|
|
|
func carrierPLMNCandidates(imsi string, mncLength int) []string {
|
|
if (mncLength == 2 || mncLength == 3) && len(imsi) >= 3+mncLength {
|
|
return []string{imsi[:3+mncLength]}
|
|
}
|
|
result := make([]string, 0, 2)
|
|
for _, length := range []int{6, 5} {
|
|
if len(imsi) >= length {
|
|
result = append(result, imsi[:length])
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func firstMatchingValue(values []string, match func(string) bool) string {
|
|
for _, value := range values {
|
|
if match(value) {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func containsString(values []string, wanted string) bool {
|
|
for _, value := range values {
|
|
if value == wanted {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func containsFold(values []string, wanted string) bool {
|
|
for _, value := range values {
|
|
if strings.EqualFold(value, wanted) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func prefixFold(value, prefix string) bool {
|
|
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(value)), strings.ToLower(strings.TrimSpace(prefix)))
|
|
}
|
|
|
|
func imsiPatternMatch(imsi, pattern string) bool {
|
|
pattern = strings.TrimSpace(pattern)
|
|
if len(imsi) < len(pattern) {
|
|
return false
|
|
}
|
|
for index, value := range pattern {
|
|
if value != 'x' && value != 'X' && byte(value) != imsi[index] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|