@@ -5,40 +5,149 @@ import { join } from 'node:path'
55import { binarySearch , getSmallMemoryFile , type IpLocationApiSettings , type LocalDatabase , number37ToString , parseIp , SAVED_SETTINGS } from '@iplookup/util'
66import { LOADED_DATA } from './reload.js'
77
8+ /**
9+ * Represents geographical data for an IP address
10+ */
811interface GeoData {
12+ /**
13+ * The approximate WGS84 latitude of the IP address
14+ *
15+ * @see https://en.wikipedia.org/wiki/World_Geodetic_System
16+ */
917 latitude ?: number
18+ /**
19+ * The approximate WGS84 longitude of the IP address
20+ *
21+ * @see https://en.wikipedia.org/wiki/World_Geodetic_System
22+ */
1023 longitude ?: number
24+ /**
25+ * The region-specific postcode nearest to the IP address
26+ */
1127 postcode ?: string
28+ /**
29+ * The radius in kilometers of the specified location where the IP address is likely to be
30+ */
1231 area ?: number
32+ /**
33+ * The country of the IP address
34+ * @example 'US'
35+ * @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
36+ */
1337 country ?: string
38+ /**
39+ * Whether the country is a member of the European Union
40+ *
41+ * @requires country
42+ */
1443 eu ?: boolean
44+ /**
45+ * The first region of the IP address
46+ * @example 'NL-ZH' // (Netherlands, South Holland)
47+ * @see https://en.wikipedia.org/wiki/ISO_3166-2
48+ */
1549 region1 ?: string
50+ /**
51+ * The name of the first region of the IP address (Can be aquired in multiple languages using the `language` setting)
52+ * @example 'South Holland'
53+ */
1654 region1_name ?: string
55+ /**
56+ * The second region of the IP address
57+ * @example 'NL-ZH' // (Netherlands, South Holland)
58+ * @see https://en.wikipedia.org/wiki/ISO_3166-2
59+ */
1760 region2 ?: string
61+ /**
62+ * The name of the second region of the IP address (Can be aquired in multiple languages using the `language` setting)
63+ * @example 'South Holland'
64+ */
1865 region2_name ?: string
66+ /**
67+ * The metropolitan area of the IP address (Google AdWords API)
68+ * @example 1000
69+ * @see https://developers.google.com/adwords/api/docs/appendix/cities-DMAregions
70+ */
1971 metro ?: number
72+ /**
73+ * The timezone of the IP address
74+ * @example 'Europe/Amsterdam'
75+ */
2076 timezone ?: string
77+ /**
78+ * The city of the IP address
79+ * @example 'Amsterdam'
80+ */
2181 city ?: string
82+ /**
83+ * The name of the country of the IP address (In English)
84+ * @example 'Netherlands'
85+ * @requires country
86+ * @requires country-list (optional peer dependency)
87+ */
2288 country_name ?: string
89+ /**
90+ * The name of the country of the IP address (In the native language of the country)
91+ * @example 'Nederland'
92+ * @requires country
93+ * @requires country-list (optional peer dependency)
94+ */
2395 country_native ?: string
96+ /**
97+ * The continent of the IP address (alpha-2 code)
98+ * @example 'EU'
99+ * @requires country
100+ * @requires country-list (optional peer dependency)
101+ */
24102 continent ?: string
103+ /**
104+ * The name of the continent of the IP address (In English)
105+ * @example 'Europe'
106+ * @requires country
107+ * @requires country-list (optional peer dependency)
108+ */
25109 continent_name ?: string
110+ /**
111+ * The capital of the country of the IP address (In English)
112+ * @example 'Amsterdam'
113+ * @requires country
114+ * @requires country-list (optional peer dependency)
115+ */
26116 capital ?: string
117+ /**
118+ * The phone codes of the country of the IP address
119+ * @example ['31']
120+ * @requires country
121+ * @requires country-list (optional peer dependency)
122+ */
27123 phone ?: number [ ]
124+ /**
125+ * The currency of the country of the IP address
126+ * @example ['EUR']
127+ * @requires country
128+ * @requires country-list (optional peer dependency)
129+ */
28130 currency ?: string [ ]
131+ /**
132+ * The languages of the country of the IP address
133+ * @example ['nl']
134+ */
29135 languages ?: string [ ]
30136}
31137
138+ /**
139+ * Looks up geographical data for a given IP address
140+ * @param ip - The IP address to look up
141+ * @returns A Promise that resolves to GeoData or null if not found
142+ */
32143export async function lookup ( ip : string ) : Promise < GeoData | null > {
33144 //* We don't use net.isIP(ip) as it is slow for ipv6
34145 const { version, ip : ipNumber } = parseIp ( ip )
35146
36147 const settings = SAVED_SETTINGS
37148 const db = version === 4 ? settings . v4 : settings . v6
38149
39- if ( ! db . loadedData )
40- return null
41- if ( ! ( ipNumber >= db . loadedData . firstIp ) )
150+ if ( ! db . loadedData || ! ( ipNumber >= db . loadedData . firstIp ) )
42151 return null
43152 const list = db . loadedData . startIps
44153 const line = binarySearch ( list , ipNumber )
@@ -71,6 +180,13 @@ export async function lookup(ip: string): Promise<GeoData | null> {
71180 return setCityInfo ( db . loadedData . mainBuffer ! , line * db . recordSize , settings )
72181}
73182
183+ /**
184+ * Reads a specific line from a file in the database
185+ * @param line - The line number to read
186+ * @param db - The database object
187+ * @param settings - The API settings
188+ * @returns A Promise that resolves to a Buffer containing the line data
189+ */
74190async function lineToFile ( line : number , db : LocalDatabase , settings : IpLocationApiSettings ) : Promise < Buffer > {
75191 const [ dir , file , offset ] = getSmallMemoryFile ( line , db )
76192 const fd = await open ( join ( settings . fieldDir , dir , file ) , 'r' )
@@ -82,13 +198,24 @@ async function lineToFile(line: number, db: LocalDatabase, settings: IpLocationA
82198 return buffer
83199}
84200
201+ /**
202+ * Extracts city information from a buffer
203+ * @param buffer - The buffer containing city data
204+ * @param offset - The starting offset in the buffer
205+ * @param settings - The API settings
206+ * @returns A Promise that resolves to GeoData
207+ */
85208function setCityInfo ( buffer : Buffer , offset : number , settings : IpLocationApiSettings ) : Promise < GeoData > {
86209 let locationId : number | undefined
87210 const geodata : GeoData = { }
211+
212+ //* Read location ID if location file is available
88213 if ( settings . locationFile ) {
89214 locationId = buffer . readUInt32LE ( offset )
90215 offset += 4
91216 }
217+
218+ //* Read latitude and longitude if included in fields
92219 if ( settings . fields . includes ( 'latitude' ) ) {
93220 geodata . latitude = buffer . readInt32LE ( offset ) / 10000
94221 offset += 4
@@ -97,6 +224,8 @@ function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSett
97224 geodata . longitude = buffer . readInt32LE ( offset ) / 10000
98225 offset += 4
99226 }
227+
228+ //* Read and decode postcode if included in fields
100229 if ( settings . fields . includes ( 'postcode' ) ) {
101230 const postcodeLength = buffer . readUInt32LE ( offset )
102231 const postcodeValue = buffer . readInt8 ( offset + 4 )
@@ -128,6 +257,8 @@ function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSett
128257 }
129258 offset += 5
130259 }
260+
261+ //* Read area if included in fields
131262 if ( settings . fields . includes ( 'area' ) ) {
132263 const areaMap = LOADED_DATA . sub ?. area
133264 if ( areaMap ) {
@@ -136,6 +267,7 @@ function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSett
136267 // offset += 1
137268 }
138269
270+ //* Process location data if locationId is available
139271 if ( locationId ) {
140272 let locationOffset = ( locationId - 1 ) * settings . locationRecordSize
141273 const locationBuffer = LOADED_DATA . location
@@ -207,16 +339,30 @@ function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSett
207339 return setCountryInfo ( geodata , settings )
208340}
209341
342+ /**
343+ * Pads a string with leading zeros
344+ * @param text - The text to pad
345+ * @param length - The desired length of the padded string
346+ * @returns The zero-padded string
347+ */
210348function getZeroFill ( text : string , length : number ) {
211349 return '0' . repeat ( length - text . length ) + text
212350}
213351
352+ /**
353+ * Adds additional country information to the GeoData object
354+ * @param geodata - The GeoData object to enhance
355+ * @param settings - The API settings
356+ * @returns A Promise that resolves to the enhanced GeoData
357+ */
214358async function setCountryInfo ( geodata : GeoData , settings : IpLocationApiSettings ) : Promise < GeoData > {
215359 if ( settings . addCountryInfo && geodata . country ) {
216360 //* Import the countries-list package (optional peer dependency)
217361 try {
218362 const { countries, continents } = await import ( 'countries-list' )
219363 const country = countries [ geodata . country as keyof typeof countries ] as ICountry | undefined
364+
365+ //* Enhance geodata with additional country information
220366 geodata . country_name = country ?. name
221367 geodata . country_native = country ?. native
222368 geodata . continent = country ?. continent ? continents [ country . continent ] : undefined
0 commit comments