Created
March 27, 2014 13:43
-
-
Save jgrahamc/9807839 to your computer and use it in GitHub Desktop.
DNS LOC textual record parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
DNS LOC