Documentation
¶
Overview ¶
Package devicex resolves a device model code to the device it names.
A User-Agent from an Android phone carries a model code — "SM-S928B", "CPH2451", "2201116SG" — and nothing else about the hardware. The code is evidence; the handset's name is not derivable from it. Samsung assigned "SM-S928B" to the Galaxy S24 Ultra, and no amount of pattern matching recovers that. It requires a catalogue.
d, ok := devicex.Lookup("SM-G973F")
// d.Brand = "Samsung", d.Name = "Galaxy S10"
This package is the catalogue and the rules that read a model code's shape. It parses no User-Agents, has no dependencies, and holds no state. It is separate from the parser that usually consumes it so that the data can be updated on its own schedule: phones ship weekly, parsers do not.
Lookup is the catalogue alone. Resolve is everything the package knows about one request — the catalogue, the code-shape rules, and the rules for hardware that carries no model code at all:
r, ok := devicex.Resolve(devicex.Code(field), ua) // r.Name, r.Brand, r.Model, r.Type, r.Family, r.Confidence, and r.ID
What it will not do ¶
A code the catalogue does not hold returns ok == false. It never returns an approximation, a nearest match or a guess derived from the code's shape, because a wrong device name is worse than no device name — it is a fact- shaped value that a caller will store, aggregate and report.
Brand is a separate question and is answerable without this package: model codes have manufacturer-specific shapes, so "SM-" implies Samsung whether or not the specific handset is known. See BrandOf.
Provenance ¶
Every entry comes from a published catalogue, cited in Source. Nothing here is remembered or inferred. See NOTICE.md.
Index ¶
- Constants
- Variables
- func All(fn func(Device) bool)
- func BrandOf(code string) (string, bool)
- func Brands() []string
- func Code(field string) string
- func Describe(code, ua string) (name, brand, model, family, deviceType string, confidence float64, ok bool)
- func Len() int
- func Names(code string) (name, brand string, ok bool)
- func Version() string
- type Device
- type DeviceType
- type MatchKind
- type Rule
Constants ¶
const Generated = catalog.Generated
Generated is the date of the snapshot the catalogue was built from, YYYY-MM-DD. It is the age of the data, not of the release: a catalogue imported from a source that stopped updating carries the source's date, which is what decides which handsets it cannot name.
const Source = catalog.Source
Source cites where the catalogue came from.
Variables ¶
var Rules = []Rule{ {ID: "pixel_9_pro_xl", Name: "Pixel 9 Pro XL", Brand: "Google", Model: "Pixel 9 Pro XL", Family: "Pixel", Type: TypeMobile, Priority: 240, Match: MatchToken, Value: "Pixel 9 Pro XL", Confidence: 0.98}, {ID: "pixel_9_pro", Name: "Pixel 9 Pro", Brand: "Google", Model: "Pixel 9 Pro", Family: "Pixel", Type: TypeMobile, Priority: 239, Match: MatchToken, Value: "Pixel 9 Pro", Confidence: 0.98}, {ID: "pixel_9", Name: "Pixel 9", Brand: "Google", Model: "Pixel 9", Family: "Pixel", Type: TypeMobile, Priority: 238, Match: MatchToken, Value: "Pixel 9", Confidence: 0.98}, {ID: "pixel_8_pro", Name: "Pixel 8 Pro", Brand: "Google", Model: "Pixel 8 Pro", Family: "Pixel", Type: TypeMobile, Priority: 237, Match: MatchToken, Value: "Pixel 8 Pro", Confidence: 0.98}, {ID: "pixel_8a", Name: "Pixel 8a", Brand: "Google", Model: "Pixel 8a", Family: "Pixel", Type: TypeMobile, Priority: 236, Match: MatchToken, Value: "Pixel 8a", Confidence: 0.98}, {ID: "pixel_8", Name: "Pixel 8", Brand: "Google", Model: "Pixel 8", Family: "Pixel", Type: TypeMobile, Priority: 235, Match: MatchToken, Value: "Pixel 8", Confidence: 0.98}, {ID: "pixel_7_pro", Name: "Pixel 7 Pro", Brand: "Google", Model: "Pixel 7 Pro", Family: "Pixel", Type: TypeMobile, Priority: 234, Match: MatchToken, Value: "Pixel 7 Pro", Confidence: 0.98}, {ID: "pixel_7a", Name: "Pixel 7a", Brand: "Google", Model: "Pixel 7a", Family: "Pixel", Type: TypeMobile, Priority: 233, Match: MatchToken, Value: "Pixel 7a", Confidence: 0.98}, {ID: "pixel_7", Name: "Pixel 7", Brand: "Google", Model: "Pixel 7", Family: "Pixel", Type: TypeMobile, Priority: 232, Match: MatchToken, Value: "Pixel 7", Confidence: 0.98}, {ID: "pixel_6_pro", Name: "Pixel 6 Pro", Brand: "Google", Model: "Pixel 6 Pro", Family: "Pixel", Type: TypeMobile, Priority: 231, Match: MatchToken, Value: "Pixel 6 Pro", Confidence: 0.98}, {ID: "pixel_6a", Name: "Pixel 6a", Brand: "Google", Model: "Pixel 6a", Family: "Pixel", Type: TypeMobile, Priority: 230, Match: MatchToken, Value: "Pixel 6a", Confidence: 0.98}, {ID: "pixel_6", Name: "Pixel 6", Brand: "Google", Model: "Pixel 6", Family: "Pixel", Type: TypeMobile, Priority: 229, Match: MatchToken, Value: "Pixel 6", Confidence: 0.98}, {ID: "iphone", Name: "iPhone", Brand: "Apple", Model: "iPhone", Type: TypeMobile, Priority: 200, Match: MatchToken, Value: "iPhone", Confidence: 0.97}, {ID: "ipad", Name: "iPad", Brand: "Apple", Model: "iPad", Type: TypeTablet, Priority: 200, Match: MatchToken, Value: "iPad", Confidence: 0.97}, {ID: "ipod", Name: "iPod touch", Brand: "Apple", Model: "iPod touch", Type: TypeMobile, Priority: 200, Match: MatchToken, Value: "iPod touch", Confidence: 0.95}, {ID: "nintendo_switch", Name: "Nintendo Switch", Brand: "Nintendo", Model: "Nintendo Switch", Type: TypeConsole, Priority: 200, Match: MatchContains, Value: "Nintendo Switch", Confidence: 0.97}, {ID: "playstation_5", Name: "PlayStation 5", Brand: "Sony", Model: "PlayStation 5", Type: TypeConsole, Priority: 200, Match: MatchContains, Value: "PlayStation 5", Confidence: 0.97}, {ID: "playstation_4", Name: "PlayStation 4", Brand: "Sony", Model: "PlayStation 4", Type: TypeConsole, Priority: 200, Match: MatchContains, Value: "PlayStation 4", Confidence: 0.97}, {ID: "xbox_console", Name: "Xbox", Brand: "Microsoft", Model: "Xbox", Type: TypeConsole, Priority: 190, Match: MatchContains, Value: "Xbox", Confidence: 0.95}, {ID: "macintosh", Name: "Mac", Brand: "Apple", Type: TypeDesktop, Priority: 150, Match: MatchToken, Value: "Macintosh", Confidence: 0.95}, {ID: "playstation", Brand: "Sony", Type: TypeConsole, Priority: 145, Match: MatchContains, Value: "PlayStation", Confidence: 0.90}, {ID: "samsung_prefix_sm_t", Brand: "Samsung", Family: "Galaxy Tab", Type: TypeTablet, Priority: 110, Match: MatchPrefix, Value: "SM-T", Confidence: 0.93}, {ID: "samsung_prefix_sm_x", Brand: "Samsung", Family: "Galaxy Tab", Type: TypeTablet, Priority: 110, Match: MatchPrefix, Value: "SM-X", Confidence: 0.93}, {ID: "samsung_prefix_sm_p", Brand: "Samsung", Family: "Galaxy Tab", Type: TypeTablet, Priority: 110, Match: MatchPrefix, Value: "SM-P", Confidence: 0.93}, {ID: "samsung_prefix_sm", Brand: "Samsung", Priority: 100, Match: MatchPrefix, Value: "SM-", Confidence: 0.93}, {ID: "samsung_prefix_sm_underscore", Brand: "Samsung", Priority: 100, Match: MatchPrefix, Value: "SM_", Confidence: 0.90}, {ID: "samsung_prefix_sch", Brand: "Samsung", Priority: 100, Match: MatchPrefix, Value: "SCH-", Confidence: 0.90}, {ID: "samsung_prefix_sph", Brand: "Samsung", Priority: 100, Match: MatchPrefix, Value: "SPH-", Confidence: 0.90}, {ID: "samsung_prefix_sgh", Brand: "Samsung", Priority: 100, Match: MatchPrefix, Value: "SGH-", Confidence: 0.90}, {ID: "samsung_prefix_gt", Brand: "Samsung", Priority: 100, Match: MatchPrefix, Value: "GT-", Confidence: 0.90}, {ID: "samsung_prefix_sc", Brand: "Samsung", Priority: 100, Match: MatchPrefix, Value: "SC-", Confidence: 0.88}, {ID: "google_prefix_pixel", Brand: "Google", Family: "Pixel", Priority: 100, Match: MatchPrefix, Value: "Pixel", Confidence: 0.95}, {ID: "realme_prefix", Brand: "realme", Priority: 100, Match: MatchPrefix, Value: "RMX", Confidence: 0.93}, {ID: "huawei_prefix_els", Brand: "Huawei", Priority: 100, Match: MatchPrefix, Value: "ELS-", Confidence: 0.92}, {ID: "huawei_prefix_ana", Brand: "Huawei", Priority: 100, Match: MatchPrefix, Value: "ANA-", Confidence: 0.92}, {ID: "huawei_prefix_vog", Brand: "Huawei", Priority: 100, Match: MatchPrefix, Value: "VOG-", Confidence: 0.92}, {ID: "huawei_prefix_lya", Brand: "Huawei", Priority: 100, Match: MatchPrefix, Value: "LYA-", Confidence: 0.92}, {ID: "huawei_prefix_noh", Brand: "Huawei", Priority: 100, Match: MatchPrefix, Value: "NOH-", Confidence: 0.92}, {ID: "motorola_prefix_moto", Brand: "Motorola", Priority: 100, Match: MatchPrefix, Value: "moto ", Confidence: 0.93}, {ID: "redmi_prefix", Brand: "Xiaomi", Family: "Redmi", Priority: 100, Match: MatchPrefix, Value: "Redmi", Confidence: 0.94}, {ID: "poco_prefix", Brand: "Xiaomi", Family: "POCO", Priority: 100, Match: MatchPrefix, Value: "POCO", Confidence: 0.94}, {ID: "mi_prefix", Brand: "Xiaomi", Priority: 100, Match: MatchPrefix, Value: "MI ", Confidence: 0.90}, {ID: "lg_prefix_lm", Brand: "LG", Priority: 100, Match: MatchPrefix, Value: "LM-", Confidence: 0.90}, {ID: "lg_prefix_lg", Brand: "LG", Priority: 100, Match: MatchPrefix, Value: "LG-", Confidence: 0.90}, {ID: "oneplus_prefix_word", Brand: "OnePlus", Priority: 100, Match: MatchPrefix, Value: "ONEPLUS", Confidence: 0.93}, {ID: "oneplus_prefix_kb2", Brand: "OnePlus", Priority: 100, Match: MatchPrefix, Value: "KB2", Confidence: 0.88}, {ID: "oneplus_prefix_le2", Brand: "OnePlus", Priority: 100, Match: MatchPrefix, Value: "LE2", Confidence: 0.88}, {ID: "sony_prefix", Brand: "Sony", Priority: 100, Match: MatchPrefix, Value: "XQ-", Confidence: 0.90}, {ID: "amazon_prefix", Brand: "Amazon", Type: TypeTablet, Priority: 100, Match: MatchPrefix, Value: "KF", Confidence: 0.88}, {ID: "vivo_prefix", Brand: "vivo", Priority: 100, Match: MatchPrefix, Value: "vivo ", Confidence: 0.93}, {ID: "asus_prefix", Brand: "Asus", Priority: 100, Match: MatchPrefix, Value: "ASUS_", Confidence: 0.93}, {ID: "nokia_prefix", Brand: "Nokia", Priority: 100, Match: MatchPrefix, Value: "Nokia", Confidence: 0.93}, {ID: "lenovo_prefix", Brand: "Lenovo", Priority: 100, Match: MatchPrefix, Value: "Lenovo", Confidence: 0.93}, {ID: "tecno_prefix", Brand: "Tecno", Priority: 100, Match: MatchPrefix, Value: "TECNO", Confidence: 0.93}, {ID: "infinix_prefix", Brand: "Infinix", Priority: 100, Match: MatchPrefix, Value: "Infinix", Confidence: 0.93}, {ID: "oppo_prefix_cph", Brand: "OPPO", Priority: 95, Match: MatchPrefix, Value: "CPH", Confidence: 0.80}, {ID: "oppo_prefix_pbe", Brand: "OPPO", Priority: 95, Match: MatchPrefix, Value: "PBE", Confidence: 0.80}, {ID: "oppo_prefix_pcl", Brand: "OPPO", Priority: 95, Match: MatchPrefix, Value: "PCL", Confidence: 0.80}, {ID: "motorola_prefix_xt", Brand: "Motorola", Priority: 95, Match: MatchPrefix, Value: "XT", Confidence: 0.88}, {ID: "xiaomi_prefix_m2", Brand: "Xiaomi", Priority: 90, Match: MatchPrefix, Value: "M2", Confidence: 0.85}, {ID: "xiaomi_numeric_prefix", Brand: "Xiaomi", Priority: 85, Match: MatchPrefix, Value: "22", Confidence: 0.80}, }
Rules is every hand-written device rule, ordered by descending priority so the first match is the most specific.
Order within one priority is significant and hand-chosen: a longer prefix is listed before a shorter one it would otherwise shadow.
Functions ¶
func All ¶
All calls fn for every device, in code order. It is a range function rather than a returned slice because the catalogue is large and copying it to answer "which devices does Samsung make" would allocate megabytes.
Returning false stops the iteration.
func BrandOf ¶
BrandOf reports the manufacturer a model code belongs to, and whether the code's shape identified one.
The catalogue is consulted first, because a recorded device is stronger evidence than a prefix. Only when the code is unknown do the shape rules decide, which is what lets a handset released after this release still resolve its maker.
An unrecognised shape returns "" and false. It never falls back to the most common manufacturer, and never infers a brand from a code's length or character mix.
func Code ¶
Code extracts the model code from the device field of an Android User-Agent.
Android writes the build fingerprint after the model, separated by a space:
"SM-A546E Build/UP1A.231005.007" -> "SM-A546E"
The suffix is a build identifier, not part of the code, and leaving it on turns every catalogue lookup into a miss. Stripping it is a fact about the shape of a model code, which is this package's subject, so it lives here rather than being re-derived by each parser.
A field with no build suffix is returned trimmed and otherwise unchanged. Codes containing spaces — "moto g play (2023)", "Pixel 9 Pro XL" — survive, because only a " Build/" boundary is cut.
func Describe ¶
func Describe(code, ua string) (name, brand, model, family, deviceType string, confidence float64, ok bool)
Describe is Names with everything the rules know, in the same dependency-free shape.
Names only reaches the catalogue, which makes it silent about most of what this package can answer: a handset newer than the catalogue resolves no brand, an iPhone resolves nothing at all, and a catalogued Galaxy Tab loses the fact that it is a tablet. Describe is Resolve behind a signature that mentions no type from this package, so a consumer can still accept it as a function value without importing devicex:
type Config struct {
Device func(code, ua string) (name, brand, model, family, deviceType string, confidence float64, ok bool)
}
p := uax.New(uax.Config{Device: devicex.Describe})
A caller willing to import devicex should call Resolve instead and read the Rule, which additionally carries the rule ID that produced the answer — the thing a bug report needs to name. This flat form exists for the caller who will not.
deviceType is one of "Mobile", "Tablet", "Desktop", "Console", or empty when no rule could tell. It is never defaulted to "Mobile".
model is the exact code the answer is about, and is empty when the rule names a class of machine rather than a model. That distinction is carried here rather than left to the caller because it cannot be recovered downstream: "iPhone" is both the name and the model an iPhone request carries, while "Macintosh" names a class and no model at all, and any heuristic that copies name into model when the User-Agent contains it invents a "Mac" model for every desktop.
func Names ¶
Names resolves a model code for a caller that only wants the two strings.
The signature is deliberately plain — no types from this package appear in it — so a consumer can accept it as a function value without importing devicex at all:
// in the consumer, with no dependency on devicex:
type Config struct {
DeviceNames func(code string) (name, brand string, ok bool)
}
// in the application, which imports both:
p := uax.New(uax.Config{DeviceNames: devicex.Names})
That keeps the catalogue and its consumers independent in both directions: neither module imports the other, and an application that does not want 25,000 device names does not link them.
It allocates nothing: the returned strings are compile-time constants.
func Version ¶
func Version() string
Version is a fingerprint of the catalogue: its snapshot date and the number of devices it holds, "2026-08-08/25475".
A caller that stored a device name six months ago and wants to know what it was decided from needs the data's identity, and Source alone does not carry it — the same source yields a different catalogue every time it is re-imported. Log this beside a resolved name and the answer stays explainable after the catalogue moves.
Types ¶
type Device ¶
type Device struct {
// Code is the model code as it appears in a User-Agent.
Code string
// Brand is the manufacturer, normalised to one spelling per company.
// Sub-brands report their parent: a Redmi Note is made by Xiaomi.
Brand string
// Name is what the device is sold as, for example "Galaxy S24 Ultra".
Name string
}
Device is a handset the catalogue knows.
func At ¶ added in v0.3.0
At returns the i'th device in code order. It reports false when i is out of range.
Len and At together let a caller address one device without materialising the catalogue: picking a random handset needs an index, and All, being iteration only, forces a caller to spill all 25,475 entries into a slice first. At costs nothing.
func Lookup ¶
Lookup returns the device a model code names.
The comparison is exact. Model codes are case-significant and are copied verbatim out of the User-Agent, so a case-insensitive match would be a different, weaker claim; use LookupFold when the input has been through something that changed its case.
func LookupFold ¶
LookupFold is Lookup, ignoring case. It is slower — the catalogue is sorted case-sensitively, so this scans — and should only be used when the caller knows the input's case is unreliable.
type DeviceType ¶
type DeviceType string
DeviceType is the form factor a rule asserts.
const ( TypeMobile DeviceType = "Mobile" TypeTablet DeviceType = "Tablet" TypeDesktop DeviceType = "Desktop" TypeConsole DeviceType = "Console" )
Form factors. A rule that cannot tell leaves Type empty rather than defaulting to Mobile.
type MatchKind ¶
type MatchKind uint8
MatchKind is how a rule compares itself against a model code or a User-Agent.
const ( // MatchToken is an exact, case-sensitive comparison against the model // code. MatchToken MatchKind = iota // MatchPrefix recognises a manufacturer's code space. It claims the maker, // never the specific handset. MatchPrefix // MatchContains looks for the value anywhere in the User-Agent. It exists // for hardware that carries no model code at all: a console names itself // in prose. MatchContains )
type Rule ¶
type Rule struct {
// ID is stable and unique; it is what a changelog or a bug report names.
ID string
// Name is the marketing name. Empty on a shape rule, which is the whole
// point of the tier split: recognising "SM-" says Samsung built it, not
// which phone it is.
Name string
// Brand is the manufacturer, normalised to one spelling per company.
Brand string
// Model is the exact code this rule names. Empty on a shape rule.
Model string
// Family is the product line, when the rule identifies one.
Family string
// Type is the form factor, empty when the rule cannot tell.
Type DeviceType
// Priority orders the rules; higher is tried first.
Priority int
// Match is how the rule recognises a code.
Match MatchKind
// Value is what Match compares against.
Value string
// Confidence is how strong the claim is, 0..1.
Confidence float64
}
Rule is one device rule.
func Resolve ¶
Resolve answers everything the package knows about one request's device evidence: the model code, and the User-Agent it came out of.
Either argument may be empty. A code alone is the Android case; a User-Agent alone covers the hardware that publishes no code.
The catalogue decides first, because a recorded device is stronger evidence than a code's shape — it names the specific handset, which no rule can. When it answers, the rules are still consulted for what a catalogue row does not carry: the form factor and the product line. That is how a Galaxy Tab resolves as a tablet while keeping its catalogued name.
The returned Rule for a catalogued device has ID "catalog" and cites Source. A code the catalogue does not hold falls through to the shape rules, and then to the User-Agent. Nothing is guessed at any step: an unrecognised code and an unrecognised User-Agent return false.
func ResolveCode ¶
ResolveCode resolves a model code against the rules.
It reads the shape of the code and nothing else, so it answers for hardware the catalogue has never heard of: a Samsung handset released after this release still resolves its maker from "SM-". It does not consult the catalogue — use Resolve for that.
MatchContains rules are skipped. They describe prose in a User-Agent ("PlayStation 5"), and a model code is not prose.
func ResolveUA ¶
ResolveUA resolves a whole User-Agent against the rules.
It exists for hardware that carries no model code at all and names itself in prose instead: "Macintosh", "PlayStation 5", "Nintendo Switch". Those tokens are the entire identifier such a request carries.
Both MatchContains and MatchToken rules are tested, because a named token is a literal substring of the User-Agent that carries it. Prefix rules are not: "SM-" appears in a User-Agent only as part of a model code, and matching it against the whole string would claim Samsung for any text containing those three characters.
Rule order decides overlaps, which is why "Pixel 9 Pro XL" is listed above "Pixel 9" — the first match on a Pixel 9 Pro XL User-Agent is the specific one.