Skip to content

Instantly share code, notes, and snippets.

@jgrahamc
Created March 27, 2014 13:43
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jgrahamc/9807839 to your computer and use it in GitHub Desktop.
Save jgrahamc/9807839 to your computer and use it in GitHub Desktop.
DNS LOC textual record parser
// loc_parser: functions to parse the textual part of a LOC record
// stored in our DNS. The key function here is parseLOCString which
// should be passed a dns.LOC and a string containing the latitude,
// longitude etc.
//
// This is an implementation of RFC 1876. Read it for background as
// the format in a dns.LOC is slightly unusual.
//
// Copyright (c) 2014 CloudFlare, Inc.
package loc
import (
"github.com/cloudflare/dns"
"regexp"
"strconv"
)
// locReD is the regexp to capture a value in a latitude or longitude.
//
// locReM is for the other values (they can be negative) and can have
// an optional 'm' after. locReOM is an optional version of
// locReM. Note that the m character after a number has no meaning at
// all, the values are always in metres.
var locReD = "(\\d+)(?: (\\d+))?(?: (\\d+(?:\\.\\d+)?))?"
var locReM = "(?: (-?\\d+(?:\\.\\d+)?)m?)"
var locReOM = locReM + "?"
var locReString = locReD + " (N|S) " + locReD + " (E|W)" + locReM +
locReOM + locReOM + locReOM
// Note that we are ignoring the error on the Compile() here. The
// regular expression is static and this will compile. If it doesn't
// the entire program is borked.
var locRe, _ = regexp.Compile(locReString)
// parseSizePrecision parses the siz, hp and vp parts of a LOC string
// and returns them in the weird 8 bit format required. See RFC 1876
// for specification and justification. The p string contains the LOC
// value to parse. It may be empty in which case the default value d
// is returned. The boolean return is false if the parsing fails.
func parseSizePrecision(p string, d uint8) (uint8, bool) {
if p == "" {
return d, true
}
f, err := strconv.ParseFloat(p, 64)
if err != nil || f < 0 || f > 90000000 {
return 0, false
}
// Conversion from m to cm
f *= 100
var exponent uint8 = 0
for f >= 10 {
exponent += 1
f /= 10
}
// Here both f and exponent will be in the range 0 to 9 and these
// get packed into a byte in the following manner. The result?
// Look at the value in hex and you can read it. e.g. 6e3 (i.e.
// 6,000) is 0x63.
return uint8(f) << 4 + exponent, true
}
// parseLatLong parses a latitude/longitude string (see ParseString
// below for format) and returns the value as a single uint32. If the
// bool value is false there was a problem with the format. The limit
// parameter specifies the limit for the number of degrees.
func parseLatLong(d, m, s string, limit uint64) (uint32, bool) {
n, err := strconv.ParseUint(d, 10, 8)
if err != nil || n > limit {
return 0, false
}
pos := float64(n) * 60
if m != "" {
n, err = strconv.ParseUint(m, 10, 8)
if err != nil || n > 59 {
return 0, false
}
pos += float64(n)
}
pos *= 60
if s != "" {
f, err := strconv.ParseFloat(s, 64)
if err != nil || f > 59.999 {
return 0, false
}
pos += f
}
pos *= 1000
return uint32(pos), pos <= float64(limit * dns.LOC_DEGREES)
}
// parseLOCString parses the string representation of a LOC record and
// fills in the fields in a newly created LOC appropriately. If the
// function returns nil then there was a parsing error, otherwise
// returns a pointer to a new LOC.
//
// Worth reading RFC 1876, Appendix A to understand.
func parseLOCString(l string) *dns.LOC {
loc := new(dns.LOC)
// The string l will be in the following format:
//
// d1 [m1 [s1]] {"N"|"S"} d2 [m2 [s2]] {"E"|"W"}
// alt["m"] [siz["m"] [hp["m"] [vp["m"]]]]
//
// d1 is the latitude, d2 is the longitude, alt is the altitude,
// siz is the size of the planet, hp and vp are the horiz and vert
// precisions. See RFC 1876 for full detail.
//
// Examples:
//
// 42 21 54 N 71 06 18 W -24m 30m
// 42 21 43.952 N 71 5 6.344 W -24m 1m 200m
// 52 14 05 N 00 08 50 E 10m
// 2 7 19 S 116 2 25 E 10m
// 42 21 28.764 N 71 00 51.617 W -44m 2000m
// 59 N 10 E 15.0 30.0 2000.0 5.0
parts := locRe.FindStringSubmatch(l)
if parts == nil {
return nil
}
// Quick reference to the matches
//
// parts[1] == latitude degrees
// parts[2] == latitude minutes (optional)
// parts[3] == latitude seconds (optional)
// parts[4] == N or S
//
// parts[5] == longitude degrees
// parts[6] == longitude minutes (optional)
// parts[7] == longitude seconds (optional)
// parts[8] == E or W
//
// parts[9] == altitude
//
// These are completely optional:
//
// parts[10] == size
// parts[11] == horizontal precision
// parts[12] == vertical precision
// Convert latitude and longitude to a 32-bit unsigned integer
latitude, ok := parseLatLong(parts[1], parts[2], parts[3], 90)
if !ok {
return nil
}
loc.Latitude = dns.LOC_EQUATOR
if parts[4] == "N" {
loc.Latitude += latitude
} else {
loc.Latitude -= latitude
}
longitude, ok := parseLatLong(parts[5], parts[6], parts[7], 180)
if !ok {
return nil
}
loc.Longitude = dns.LOC_PRIMEMERIDIAN
if parts[8] == "E" {
loc.Longitude += longitude
} else {
loc.Longitude -= longitude
}
// Now parse the altitude. Seriously, read RFC 1876 if you want to
// understand all the values and conversions here. But altitudes
// are unsigned 32-bit numbers that start 100,000m below 'sea
// level' and are expressed in cm.
//
// == (2^32-1)/100
// - 100,000
// == 42949672.95
// - 100000
// == 42849672.95
f, err := strconv.ParseFloat(parts[9], 64)
if err != nil || f < -dns.LOC_ALTITUDEBASE || f > 42849672.95 {
return nil
}
loc.Altitude = (uint32)((f + dns.LOC_ALTITUDEBASE) * 100)
// Default values for the optional components, see RFC 1876 for
// this weird encoding. But top nibble is mantissa, bottom nibble
// is exponent. Values are in cm. So, for example, 0x12 means 1 *
// 10^2 or 100cm.
// 0x12 == 1e2cm == 1m
if loc.Size, ok = parseSizePrecision(parts[10], 0x12); !ok {
return nil
}
// 0x16 == 1e6cm == 10,000m == 10km
if loc.HorizPre, ok = parseSizePrecision(parts[11], 0x16); !ok {
return nil
}
// 0x13 == 1e3cm == 10m
if loc.VertPre, ok = parseSizePrecision(parts[12], 0x13); !ok {
return nil
}
return loc
}
package loc
import (
"strings"
"testing"
)
func TestParseSizePrecision(t *testing.T) {
n, ok := parseSizePrecision("", 0x98)
if n != 0x98 || !ok {
t.Error("")
}
n, ok = parseSizePrecision("-1", 0x98)
if ok {
t.Error("")
}
n, ok = parseSizePrecision("ddd", 0x98)
if ok {
t.Error("")
}
n, ok = parseSizePrecision("100000000", 0x98)
if ok {
t.Error("")
}
n, ok = parseSizePrecision("0", 0x98)
if !ok || n != 0x00 {
t.Error("0")
}
n, ok = parseSizePrecision("0.1", 0x98)
if !ok || n != 0x11 {
t.Error("0.1")
}
n, ok = parseSizePrecision("900", 0x98)
if !ok || n != 0x94 {
t.Error("900")
}
n, ok = parseSizePrecision("900.1", 0x98)
if !ok || n != 0x94 {
t.Error("900")
}
n, ok = parseSizePrecision("12345.8", 0x98)
if !ok || n != 0x16 {
t.Error("12345.8")
}
n, ok = parseSizePrecision("90000000", 0x98)
if !ok || n != 0x99 {
t.Error("9000000")
}
}
func TestParseLatLong(t *testing.T) {
_, ok := parseLatLong("", "", "", 90)
if ok {
t.Error("")
}
_, ok = parseLatLong("a", "b", "c", 90)
if ok {
t.Error("")
}
_, ok = parseLatLong("50", "1.2", "0", 90)
if ok {
t.Error("")
}
l, ok := parseLatLong("90", "", "", 90)
if !ok || l != 90*60*60*1000 {
t.Error("90")
}
l, ok = parseLatLong("90", "", "", 89)
if ok {
t.Error("90 > 89")
}
l, ok = parseLatLong("90", "1", "0", 90)
if ok {
t.Error("90 > 90 1 0")
}
l, ok = parseLatLong("90", "0", "0", 90)
if !ok || l != 90*60*60*1000 {
t.Error("90 0 0")
}
l, ok = parseLatLong("0", "0", "0", 90)
if !ok || l != 0 {
t.Error("0 0 0")
}
l, ok = parseLatLong("89", "2", "3", 90)
if !ok || l != ((89*60+2)*60+3)*1000 {
t.Error("89 2 3")
}
l, ok = parseLatLong("54", "4", "3.8", 90)
if !ok || l != ((54*60+4)*60+3.8)*1000 {
t.Error("54 4 3.8")
}
}
func TestParseString(t *testing.T) {
l := parseLOCString("")
if l != nil {
t.Error("")
}
l = parseLOCString("random stuff")
if l != nil {
t.Error("")
}
l = parseLOCString("0 N")
if l != nil {
t.Error("")
}
l = parseLOCString("0 N 0 E 0")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31 || l.Longitude != 1<<31 || l.Altitude != 10000000 {
t.Error("0 N 0 E 0")
}
if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
t.Error("")
}
l = parseLOCString("89 N 23 E 0")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31+89*60*60*1000 || l.Longitude != 1<<31+23*60*60*1000 || l.Altitude != 10000000 {
t.Error("89 N 23 E 0")
}
if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
t.Error("")
}
l = parseLOCString("89 S 23 W 0")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31-89*60*60*1000 || l.Longitude != 1<<31-23*60*60*1000 || l.Altitude != 10000000 {
t.Error("89 N 23 E 0")
}
if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
t.Error("")
}
l = parseLOCString("89 2 1.4 N 23 6 9.8 E 0")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31+((89*60+2)*60+1.4)*1000 || l.Longitude != 1<<31+((23*60+6)*60+9.8)*1000 || l.Altitude != 10000000 {
t.Error("89 2 1.4 N 23 6 9.8 E 0")
}
if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
t.Error("")
}
l = parseLOCString("59 N 10 E 15.0 30.0 2000.0 5.0")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31+59*60*60*1000 || l.Longitude != 1<<31+10*60*60*1000 || l.Altitude != 10001500 {
t.Error("59 N 10 E 15.0 30.0 2000.0 5.0")
}
if l.Size != 0x33 || l.HorizPre != 0x25 || l.VertPre != 0x52 {
t.Error("")
}
l = parseLOCString("42 21 54 N 71 06 18 W -24m 30m")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31+((42*60+21)*60+54)*1000 || l.Longitude != 1<<31-((71*60+6)*60+18)*1000 || l.Altitude != 10000000-24*100 {
t.Error("42 21 54 N 71 06 18 W -24m 30m")
}
if l.Size != 0x33 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
t.Error("")
}
l = parseLOCString("42 21 43.952 N 71 5 6.344 W -24m 1m 200m")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31+((42*60+21)*60+43.952)*1000 || l.Longitude != 1<<31-((71*60+5)*60+6.344)*1000 || l.Altitude != 10000000-24*100 {
t.Errorf("42 21 43.952 N 71 5 6.344 W -24m 1m 200m")
}
if l.Size != 0x12 || l.HorizPre != 0x24 || l.VertPre != 0x13 {
t.Error("")
}
l = parseLOCString("52 14 05 N 00 08 50 E 10m")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31+((52*60+14)*60+5)*1000 || l.Longitude != 1<<31+((0*60+8)*60+50)*1000 || l.Altitude != 10000000+10*100 {
t.Errorf("52 14 05 N 00 08 50 E 10m")
}
if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
t.Error("")
}
l = parseLOCString("2 7 19 S 116 2 25 E 10m")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31-((2*60+7)*60+19)*1000 || l.Longitude != 1<<31+((116*60+2)*60+25)*1000 || l.Altitude != 10000000+10*100 {
t.Errorf("2 7 19 S 116 2 25 E 10m")
}
if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
t.Error("")
}
l = parseLOCString("42 21 28.764 N 71 00 51.617 W -44m 2000m")
if l == nil {
t.Error("")
}
if l.Latitude != 1<<31+((42*60+21)*60+28.764)*1000 || l.Longitude != 1<<31-((71*60+0)*60+51.617)*1000 || l.Altitude != 10000000-44*100 {
t.Errorf("42 21 28.764 N 71 00 51.617 W -44m 2000m")
}
if l.Size != 0x25 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
t.Error("")
}
}
func TestStringLOC(t *testing.T) {
l := parseLOCString("59 N 10 E 15.0 30.0 2000.0 5.0")
if l == nil {
t.Error("")
}
if !strings.HasSuffix(l.String(), "59 00 0.000 N 10 00 0.000 E 15m 30m 2000m 5m") {
t.Error("")
}
l = parseLOCString("42 21 54 N 71 06 18 W -24m 30m")
if l == nil {
t.Error("")
}
if !strings.HasSuffix(l.String(), "42 21 54.000 N 71 06 18.000 W -24m 30m 10000m 10m") {
t.Error("")
}
l = parseLOCString("42 21 43.952 N 71 5 6.344 W -24m 1m 200m")
if l == nil {
t.Error("")
}
if !strings.HasSuffix(l.String(), "42 21 43.952 N 71 05 6.344 W -24m 1m 200m 10m") {
t.Error("")
}
l = parseLOCString("52 14 05 N 00 08 50 E 10m")
if l == nil {
t.Error("")
}
if !strings.HasSuffix(l.String(), "52 14 5.000 N 00 08 50.000 E 10m 1m 10000m 10m") {
t.Error("")
}
l = parseLOCString("2 7 19 S 116 2 25 E 10m")
if l == nil {
t.Error("")
}
if !strings.HasSuffix(l.String(), "02 07 19.000 S 116 02 25.000 E 10m 1m 10000m 10m") {
t.Error("")
}
l = parseLOCString("42 21 28.764 N 71 00 51.617 W -44m 2000m")
if l == nil {
t.Error("")
}
if !strings.HasSuffix(l.String(), "42 21 28.764 N 71 00 51.617 W -44m 2000m 10000m 10m") {
t.Error("")
}
l = parseLOCString("42 21 28.764 N 71 00 51.617 W -44.55m 2000m")
if l == nil {
t.Error("")
}
if !strings.HasSuffix(l.String(), "42 21 28.764 N 71 00 51.617 W -44.55m 2000m 10000m 10m") {
t.Error(l.String())
}
}
@Redwarrio
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment