build: fix detection of openssl 3.0

Refs: #5154
Change-Id: I93ccd3d3ac0f9b8940cda76e7a6df02a65999c90
diff --git a/.waf-tools/openssl.py b/.waf-tools/openssl.py
index b35795f..47bda66 100644
--- a/.waf-tools/openssl.py
+++ b/.waf-tools/openssl.py
@@ -1,10 +1,8 @@
 #! /usr/bin/env python
 # encoding: utf-8
-# Yingdi Yu (UCLA) 2016
 
-'''
-
-When using this tool, the wscript will look like:
+"""
+When using this tool, the wscript should look like:
 
     def options(opt):
         opt.load('openssl')
@@ -15,8 +13,7 @@
 
     def build(bld):
         bld(source='main.cpp', target='app', use='OPENSSL')
-
-'''
+"""
 
 import re
 from waflib import Utils
@@ -37,55 +34,62 @@
         return None
 
 @conf
-def __openssl_find_root_and_version_file(self, *k, **kw):
-    root = k and k[0] or kw.get('path', self.options.openssl_dir)
-
-    file = self.__openssl_get_version_file(root)
-    if root and file:
+def __openssl_find_root_and_version_file(self, root):
+    if root:
+        file = self.__openssl_get_version_file(root)
+        if not file:
+            self.fatal(f'OpenSSL not found in {root}')
         return (root, file)
 
-    openssl_dir = []
+    openssl_dirs = OPENSSL_DIR
     if Utils.unversioned_sys_platform() == 'darwin':
-        openssl_dir = OPENSSL_DIR_OSX
-    else:
-        openssl_dir = OPENSSL_DIR
+        openssl_dirs = OPENSSL_DIR_OSX
 
-    if not root:
-        for dir in openssl_dir:
-            file = self.__openssl_get_version_file(dir)
-            if file:
-                return (dir, file)
+    for dir in openssl_dirs:
+        file = self.__openssl_get_version_file(dir)
+        if file:
+            return (dir, file)
 
-    if root:
-        self.fatal('OpenSSL not found in %s' % root)
-    else:
-        self.fatal('OpenSSL not found, please provide a --with-openssl=PATH argument (see help)')
+    self.fatal('OpenSSL not found, please provide a --with-openssl=PATH argument (see --help)')
+
+@conf
+def __openssl_check_version(self, version_file, atleast_version):
+    min_version = tuple(int(i) for i in atleast_version.split('.'))
+    txt = version_file.read()
+
+    # OpenSSL 3.0.0 and later
+    ver_tuple = (re.search(r'^#\s*define\s+OPENSSL_VERSION_MAJOR\s+(\d+)', txt, re.MULTILINE),
+                 re.search(r'^#\s*define\s+OPENSSL_VERSION_MINOR\s+(\d+)', txt, re.MULTILINE),
+                 re.search(r'^#\s*define\s+OPENSSL_VERSION_PATCH\s+(\d+)', txt, re.MULTILINE))
+    ver_string = re.search(r'^#\s*define\s+OPENSSL_FULL_VERSION_STR\s+"(.+)"', txt, re.MULTILINE)
+    if all(ver_tuple):
+        version = tuple(int(i[1]) for i in ver_tuple)
+        ver_string = ver_string[1] if ver_string else '.'.join(version)
+        return (version >= min_version, ver_string)
+
+    # OpenSSL 1.1.1 and earlier
+    ver_number = re.search(r'^#\s*define\s+OPENSSL_VERSION_NUMBER\s+(.+)L', txt, re.MULTILINE)
+    ver_string = re.search(r'^#\s*define\s+OPENSSL_VERSION_TEXT\s+"(.+)"', txt, re.MULTILINE)
+    if ver_number and ver_string:
+        version = int(ver_number[1], 16)
+        min_version = (min_version[0] << 28) | (min_version[1] << 20) | (min_version[2] << 12) | 0xf
+        return (version >= min_version, ver_string[1])
+
+    self.fatal(f'Cannot extract version information from {version_file}')
 
 @conf
 def check_openssl(self, *k, **kw):
     self.start_msg('Checking for OpenSSL version')
-    (root, file) = self.__openssl_find_root_and_version_file(*k, **kw)
 
-    try:
-        txt = file.read()
-        re_version = re.compile('^#\\s*define\\s+OPENSSL_VERSION_NUMBER\\s+(.*)L', re.M)
-        version_number = re_version.search(txt)
-
-        re_version_text = re.compile('^#\\s*define\\s+OPENSSL_VERSION_TEXT\\s+(.*)', re.M)
-        version_text = re_version_text.search(txt)
-
-        if version_number and version_text:
-            version = version_number.group(1)
-            self.end_msg(version_text.group(1))
-        else:
-            self.fatal('OpenSSL version file is present, but is not recognizable')
-    except:
-        self.fatal('OpenSSL version file is not found or is not usable')
-
+    path = k and k[0] or kw.get('path', self.options.openssl_dir)
+    root, version_file = self.__openssl_find_root_and_version_file(path)
     atleast_version = kw.get('atleast_version', 0)
-    if int(version, 16) < atleast_version:
-        self.fatal('The version of OpenSSL is too old\n'
-                   'Please upgrade your distribution or manually install a newer version of OpenSSL')
+    ok, version_str = self.__openssl_check_version(version_file, atleast_version)
+
+    self.end_msg(version_str)
+    if not ok:
+        self.fatal(f'The version of OpenSSL is too old; {atleast_version} or later is required.\n'
+                   'Please upgrade your distribution or manually install a newer version of OpenSSL.')
 
     if 'msg' not in kw:
         kw['msg'] = 'Checking if OpenSSL library works'
@@ -94,9 +98,8 @@
     if 'uselib_store' not in kw:
         kw['uselib_store'] = 'OPENSSL'
     if 'define_name' not in kw:
-        kw['define_name'] = 'HAVE_%s' % kw['uselib_store']
-    if root:
-        kw['includes'] = '%s/include' % root
-        kw['libpath'] = '%s/lib' % root
+        kw['define_name'] = f"HAVE_{kw['uselib_store']}"
+    kw['includes'] = f'{root}/include'
+    kw['libpath'] = f'{root}/lib'
 
     self.check_cxx(**kw)
diff --git a/wscript b/wscript
index c28bed1..966268c 100644
--- a/wscript
+++ b/wscript
@@ -104,7 +104,7 @@
 
     conf.check_osx_frameworks()
     conf.check_sqlite3()
-    conf.check_openssl(lib='crypto', atleast_version=0x1000200f) # 1.0.2
+    conf.check_openssl(lib='crypto', atleast_version='1.0.2')
 
     boost_libs = ['system', 'program_options', 'chrono', 'date_time', 'filesystem', 'thread', 'log']