Oracle PL/SQL versions of INET6_ATON and NTOA functions?

前端 未结 1 863
孤独总比滥情好
孤独总比滥情好 2021-01-06 14:56

Any have any good code for converting a IPv6 address string into an integer? Converting IPv4 seems to be fairly easy, with the one format. However, IPv6 has several differ

相关标签:
1条回答
  • 2021-01-06 15:27

    Ended up rolling my own. Also realized that Oracle's 126-bit INTEGER is not enough bits for IPv6's 128-bit addresses. Frankly, I don't know how the original C library's INET6_ATON (or INET_PTON) does it, considering that I've never heard of a 16-byte integer.

    I ended up with a 32-byte hex string, which means I have to do some fancy "half-string" math on nettohex and use SUBSTR for the FBIs to work correctly. (Blasted PL/SQL doesn't allow for "RETURN CHAR(32)"...)

    Overall, though, it works well, works in all formats, and allows for index-based character comparisons to find out if an IP address is within an IP range.

    Here's the full code:

    CREATE OR REPLACE FUNCTION ipguess(
       ip_string IN VARCHAR2
    ) RETURN NATURAL
    DETERMINISTIC
    IS
    BEGIN
       -- Short-circuit the most popular, and also catch the special case of IPv4 addresses in IPv6
       IF    REGEXP_LIKE(ip_string, '\d{1,3}(\.\d{1,3}){3}')                       THEN RETURN 4;
       ELSIF REGEXP_LIKE(ip_string, '[[:xdigit:]]{0,4}(\:[[:xdigit:]]{0,4}){0,7}') THEN RETURN 6;
       ELSE                                                                             RETURN NULL;
       END IF;
    END ipguess;
    
    CREATE OR REPLACE FUNCTION iptohex(
       ip_string IN VARCHAR2
    ) RETURN CHAR     -- INTEGER only holds 126 binary digits, IPv6 has 128
    DETERMINISTIC
    IS
       iptype NATURAL := ipguess(ip_string);
       ip     VARCHAR2(32);
       ipwork VARCHAR2(64);
       d      INTEGER;
       q      VARCHAR2(3);
    BEGIN
       IF    iptype = 4 THEN
          -- Sanity check
          ipwork := REGEXP_SUBSTR(ip_string, '\d{1,3}(\.\d{1,3}){3}');
          IF ipwork IS NULL THEN RETURN NULL; END IF;
    
          -- Starting prefix
          -- NOTE: 2^48 - 2^32 = 281470681743360 = ::ffff:0.0.0.0
          --       (for compatibility with IPv4 addresses in IPv6)
          ip := '00000000000000000000ffff';
    
          -- Parse the input
          WHILE LENGTH(ipwork) IS NOT NULL
          LOOP
             d := INSTR(ipwork, '.');  -- find the dot
             IF d > 0 THEN             -- isolate the decimal octet
                q      := SUBSTR(ipwork, 1, d - 1);
                ipwork := SUBSTR(ipwork, d + 1);
             ELSE
                q      := ipwork;
                ipwork := '';
             END IF;
    
             -- convert to a hex string
             ip := ip || TO_CHAR(TO_NUMBER(q), 'FM0x');
    
          END LOOP;
       ELSIF iptype = 6 THEN
          -- Short-circuit "::" = 0
          IF ip_string = '::' THEN RETURN LPAD('0', 32, '0'); END IF;
    
          -- Sanity check
          ipwork := REGEXP_SUBSTR(ip_string, '[[:xdigit:]]{0,4}(\:[[:xdigit:]]{0,4}){0,7}');
          IF ipwork IS NULL THEN RETURN NULL; END IF;
    
          -- Replace leading zeros
          -- (add a bunch to all of the pairs, then remove only the required ones)
          ipwork := REGEXP_REPLACE(ipwork, '(^|\:)([[:xdigit:]]{1,4})', '\1000\2');
          ipwork := REGEXP_REPLACE(ipwork, '(^|\:)0+([[:xdigit:]]{4})',    '\1\2');
    
          -- Groups of zeroes
          -- (total length should be 32+Z, so the gap would be the zeroes)
          ipwork := REPLACE(ipwork, '::', 'Z');
          ipwork := REPLACE(ipwork, ':');
          ipwork := REPLACE(ipwork, 'Z', LPAD('0', 33 - LENGTH(ipwork), '0'));
          ip     := LOWER(ipwork);
       ELSE
          RETURN NULL;
       END IF;
    
       RETURN ip;
    
    END iptohex;
    
    CREATE OR REPLACE FUNCTION nettohex(
       ip_string IN VARCHAR2,
       cidr      IN NATURALN,
       is_end    IN SIGNTYPE DEFAULT 0
    ) RETURN CHAR
    DETERMINISTIC
    IS
       iptype   NATURAL  := ipguess(ip_string);
       iphex    CHAR(32) := iptohex(ip_string);
       iphalf1  CHAR(16) := SUBSTR(iphex, 1, 16);
       iphalf2  CHAR(16) := SUBSTR(iphex, 17);
       ipwork   CHAR(16) := iphalf2;
       cidr_exp INTEGER  := 2 ** (iptype + 1) - cidr;
       ipint    INTEGER;
       subnet   INTEGER;
       is_big   SIGNTYPE := 0;
    BEGIN
       -- Sanity checks
       IF    iptype IS NULL THEN RETURN NULL;
       ELSIF iphex  IS NULL THEN RETURN NULL;
       END IF;
    
       IF    cidr_exp >= 64  THEN is_big := 1;
       ELSIF cidr_exp = 0    THEN RETURN iphex;  -- the exact IP, such as /32 on IPv4
       ELSIF cidr_exp <  0   THEN RETURN NULL;
       ELSIF cidr_exp >  128 THEN RETURN NULL;
       END IF;
    
       -- Change some variables around if we are working with the first/largest half
       IF is_big = 1 THEN
          ipwork   := iphalf1;
          iphalf2  := TO_CHAR((2 ** 64 - 1) * is_end, 'FM0xxxxxxxxxxxxxxx');  -- either all 0 or all F
          cidr_exp := cidr_exp - 64;
       END IF;
    
       -- Normalize IP to divisions of CIDR
       subnet := 2 ** cidr_exp;
       ipint  := TO_NUMBER(ipwork, 'FM0xxxxxxxxxxxxxxx');
       -- if is_end = 1 then add one net range (then subtract one IP) to get the ending range
       ipwork := TO_CHAR(FLOOR(ipint / subnet + is_end) * subnet - is_end, 'FM0xxxxxxxxxxxxxxx');
    
       -- Re-integrate
       IF is_big = 0 THEN iphalf2 := ipwork;
       ELSE               iphalf1 := ipwork;
       END IF;
    
       RETURN SUBSTR(iphalf1 || iphalf2, 1, 32);
    
    END nettohex;
    
    -- WHERE clause:
    -- 1. BETWEEN compare:
    --    iptohex(a.ip_addy) BETWEEN nettohex(b.net_addy, b.cidr, 0) AND nettohex(b.net_addy, b.cidr, 1)
    --
    --    Requires three function-based indexes, but all of them would work, as they are all inside the tables.
    --
    -- 2. CIDR match:
    --    nettohex(a.ip_addy, b.cidr) = nettohex(b.net_addy, b.cidr)
    --
    --    Only two functions and uses exact match, but first one requires an outside variable.  Last one would be only function-based index.
    --    An FBI of iptohex(a.ip_addy) could be implemented, but it's questionable if nettohex would use that index.
    --
    -- Recommended FBIs:
    --
    -- (SUBSTR(iptohex(a.ip_addy), 1, 32))
    -- (SUBSTR(nettohex(b.ip_addy, b.cidr, 0), 1, 32), SUBSTR(nettohex(b.ip_addy, b.cidr, 1), 1, 32))
    --
    -- NOTE: Will need to use the SUBSTR form for the above WHERE clauses!
    

    UPDATE: Oracle 11g does allow for the SUBSTR entry to be put a virtual column. So, you could have columns like this:

    ip              VARCHAR2(39),
    cidr            NUMBER(2),
    ip_hex          AS (SUBSTR(iptohex(ip),           1, 32)) VIRTUAL,
    ip_nethex_start AS (SUBSTR(nettohex(ip, cidr, 0), 1, 32)) VIRTUAL,
    ip_nethex_end   AS (SUBSTR(nettohex(ip, cidr, 1), 1, 32)) VIRTUAL,
    

    And indexes like:

    CREATE INDEX foobar_iphex_idx ON foobar (ip_hex);
    CREATE INDEX foobar_ipnet_idx ON foobar (ip_nethex_start, ip_nethex_end);
    

    Using WHERE clauses like:

    a.ip_hex BETWEEN b.ip_nethex_start AND b.ip_nethex_end
    nettohex(a.ip, b.cidr) = b.ip_nethex_start  -- not as effective
    
    0 讨论(0)
提交回复
热议问题