@@ -35,6 +35,7 @@ const isCorrect4 = common.isCorrect(constants.BITS);
3535 */
3636class Address4 {
3737 constructor ( address ) {
38+ this . addressMinusSuffix = '' ;
3839 this . groups = constants . GROUPS ;
3940 this . parsedAddress = [ ] ;
4041 this . parsedSubnet = '' ;
@@ -51,6 +52,15 @@ class Address4 {
5152 * @returns {boolean }
5253 */
5354 this . isInSubnet = common . isInSubnet ;
55+ /**
56+ * Returns true if this address's host bits fall inside the given subnet,
57+ * ignoring this address's own subnet mask. Prefer this over `isInSubnet`
58+ * when classifying a single address, so the answer doesn't change with the
59+ * CIDR suffix the caller happened to write — notably when the address came
60+ * from untrusted input and the result backs a trust-boundary decision.
61+ * @returns {boolean }
62+ */
63+ this . isHostInSubnet = common . isHostInSubnet ;
5464 this . address = address ;
5565 const subnet = constants . RE_SUBNET_STRING . exec ( address ) ;
5666 if ( subnet ) {
@@ -78,7 +88,7 @@ class Address4 {
7888 new Address4 ( address ) ;
7989 return true ;
8090 }
81- catch ( e ) {
91+ catch {
8292 return false ;
8393 }
8494 }
@@ -90,6 +100,11 @@ class Address4 {
90100 */
91101 parse ( address ) {
92102 const groups = address . split ( '.' ) ;
103+ // Checked before the general match so the error names the actual problem.
104+ // Address6 rejects the same notation on its v4-in-v6 path.
105+ if ( groups . some ( ( group ) => / ^ 0 \d / . test ( group ) ) ) {
106+ throw new address_error_1 . AddressError ( "IPv4 addresses can't have leading zeroes." ) ;
107+ }
93108 if ( ! address . match ( constants . RE_ADDRESS ) ) {
94109 throw new address_error_1 . AddressError ( 'Invalid IPv4 address.' ) ;
95110 }
@@ -128,7 +143,6 @@ class Address4 {
128143 static fromAddressAndWildcardMask ( address , wildcardMask ) {
129144 const wildcard = new Address4 ( wildcardMask ) . bigInt ( ) ;
130145 const allOnes = ( BigInt ( 1 ) << BigInt ( constants . BITS ) ) - BigInt ( 1 ) ;
131- // eslint-disable-next-line no-bitwise
132146 const mask = wildcard ^ allOnes ;
133147 const bits = common . prefixLengthFromMask ( mask , constants . BITS ) ;
134148 return new Address4 ( `${ address } /${ bits } ` ) ;
@@ -328,32 +342,32 @@ class Address4 {
328342 * @returns {Address4 }
329343 */
330344 static fromBigInt ( bigInt ) {
331- if ( bigInt < 0n || bigInt > 0xffffffffn ) {
345+ if ( bigInt < BigInt ( 0 ) || bigInt > BigInt ( 0xffffffff ) ) {
332346 throw new address_error_1 . AddressError ( 'IPv4 BigInt must be in the range 0 to 2**32 - 1' ) ;
333347 }
334348 return Address4 . fromHex ( bigInt . toString ( 16 ) . padStart ( 8 , '0' ) ) ;
335349 }
336350 /**
337- * Convert a byte array to an Address4 object.
351+ * Convert a byte array to an Address4 object. Throws `AddressError` unless
352+ * given exactly 4 integers from 0 to 255. Signed bytes are rejected, so
353+ * this differs from `Address6.fromByteArray`, which folds them; the two
354+ * contracts converge on this stricter form in the next major version.
338355 *
339356 * To convert from a Node.js `Buffer`, spread it: `Address4.fromByteArray([...buf])`.
340357 * @param {Array<number> } bytes - an array of 4 bytes (0-255)
341358 * @returns {Address4 }
342359 */
343360 static fromByteArray ( bytes ) {
344- if ( bytes . length !== 4 ) {
345- throw new address_error_1 . AddressError ( 'IPv4 addresses require exactly 4 bytes' ) ;
346- }
347- // Validate that all bytes are within valid range (0-255)
348- for ( let i = 0 ; i < bytes . length ; i ++ ) {
349- if ( ! Number . isInteger ( bytes [ i ] ) || bytes [ i ] < 0 || bytes [ i ] > 255 ) {
350- throw new address_error_1 . AddressError ( 'All bytes must be integers between 0 and 255' ) ;
351- }
352- }
361+ common . assertByteArray ( bytes , 4 , 'IPv4' , 0 ) ;
353362 return this . fromUnsignedByteArray ( bytes ) ;
354363 }
355364 /**
356- * Convert an unsigned byte array to an Address4 object
365+ * Convert an unsigned byte array to an Address4 object. Throws
366+ * `AddressError` unless given exactly 4 bytes, and rejects values outside
367+ * 0 to 255 when parsing the resulting address.
368+ *
369+ * To convert from a Node.js `Buffer`, spread it:
370+ * `Address4.fromUnsignedByteArray([...buf])`.
357371 * @param {Array<number> } bytes - an array of 4 unsigned bytes (0-255)
358372 * @returns {Address4 }
359373 */
@@ -383,7 +397,8 @@ class Address4 {
383397 return this . binaryZeroPad ( ) . slice ( start , end ) ;
384398 }
385399 /**
386- * Return the reversed ip6.arpa form of the address
400+ * Return the reversed in-addr.arpa form of the address, e.g.
401+ * `42.2.0.192.in-addr.arpa.` for `192.0.2.42`.
387402 * @param {Object } options
388403 * @param {boolean } options.omitSuffix - omit the "in-addr.arpa" suffix
389404 * @returns {String }
@@ -403,49 +418,49 @@ class Address4 {
403418 * @returns {boolean }
404419 */
405420 isMulticast ( ) {
406- return this . isInSubnet ( MULTICAST_V4 ) ;
421+ return this . isHostInSubnet ( MULTICAST_V4 ) ;
407422 }
408423 /**
409424 * Returns true if the address is in one of the [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private address ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`).
410425 * @returns {boolean }
411426 */
412427 isPrivate ( ) {
413- return PRIVATE_V4 . some ( ( subnet ) => this . isInSubnet ( subnet ) ) ;
428+ return PRIVATE_V4 . some ( ( subnet ) => this . isHostInSubnet ( subnet ) ) ;
414429 }
415430 /**
416431 * Returns true if the address is in the loopback range `127.0.0.0/8` ([RFC 1122](https://datatracker.ietf.org/doc/html/rfc1122)).
417432 * @returns {boolean }
418433 */
419434 isLoopback ( ) {
420- return this . isInSubnet ( LOOPBACK_V4 ) ;
435+ return this . isHostInSubnet ( LOOPBACK_V4 ) ;
421436 }
422437 /**
423438 * Returns true if the address is in the link-local range `169.254.0.0/16` ([RFC 3927](https://datatracker.ietf.org/doc/html/rfc3927)).
424439 * @returns {boolean }
425440 */
426441 isLinkLocal ( ) {
427- return this . isInSubnet ( LINK_LOCAL_V4 ) ;
442+ return this . isHostInSubnet ( LINK_LOCAL_V4 ) ;
428443 }
429444 /**
430445 * Returns true if the address is the unspecified address `0.0.0.0`.
431446 * @returns {boolean }
432447 */
433448 isUnspecified ( ) {
434- return this . isInSubnet ( UNSPECIFIED_V4 ) ;
449+ return this . isHostInSubnet ( UNSPECIFIED_V4 ) ;
435450 }
436451 /**
437452 * Returns true if the address is the limited broadcast address `255.255.255.255` ([RFC 919](https://datatracker.ietf.org/doc/html/rfc919)).
438453 * @returns {boolean }
439454 */
440455 isBroadcast ( ) {
441- return this . isInSubnet ( BROADCAST_V4 ) ;
456+ return this . isHostInSubnet ( BROADCAST_V4 ) ;
442457 }
443458 /**
444459 * Returns true if the address is in the carrier-grade NAT range `100.64.0.0/10` ([RFC 6598](https://datatracker.ietf.org/doc/html/rfc6598)).
445460 * @returns {boolean }
446461 */
447462 isCGNAT ( ) {
448- return this . isInSubnet ( CGNAT_V4 ) ;
463+ return this . isHostInSubnet ( CGNAT_V4 ) ;
449464 }
450465 /**
451466 * Returns a zero-padded base-2 string representation of the address
@@ -458,12 +473,17 @@ class Address4 {
458473 return this . _binaryZeroPad ;
459474 }
460475 /**
461- * Groups an IPv4 address for inclusion at the end of an IPv6 address
476+ * Groups an IPv4 address for inclusion at the end of an IPv6 address.
477+ *
478+ * Returns an HTML fragment: each half of the address is wrapped in a
479+ * `<span>` carrying the group classes an address-inspector UI hovers on.
480+ * The address content is HTML-escaped; anything you concatenate around it
481+ * is your responsibility.
462482 * @returns {String }
463483 */
464484 groupForV6 ( ) {
465485 const segments = this . parsedAddress ;
466- return this . address . replace ( constants . RE_ADDRESS , `<span class="hover-group group-v4 group-6">${ segments
486+ return this . correctForm ( ) . replace ( constants . RE_ADDRESS , `<span class="hover-group group-v4 group-6">${ segments
467487 . slice ( 0 , 2 )
468488 . join ( '.' ) } </span>.<span class="hover-group group-v4 group-7">${ segments
469489 . slice ( 2 , 4 )
0 commit comments