Repository: kootenpv/access_points

Issue:
"list index out of range"
```
list index out of range
Line:
                    CFInternet19                   -91  100     Y  -- WPA2(PSK/AES/AES)
Output:
                            SSID BSSID             RSSI CHANNEL HT CC SECURITY (auth/unicast/group)
                    CFInternet19                   -91  100     Y  -- WPA2(PSK/AES/AES)
                    CFInternet19                   -90  36      Y  -- WPA2(PSK/AES/AES)
                         BTWi-fi                   -87  11      Y  -- NONE
                     BTHub6-C3NH                   -87  11      Y  -- WPA2(PSK/AES/AES)
                        BTWifi-X                   -84  1       Y  -- WPA2(802.1x/AES/AES)
                        SKYDAAPC                   -82  1       Y  -- WPA2(PSK/AES/AES)
                   CFL-6BDJ-2.4G                   -61  5,+1    Y  -- WPA(PSK/AES,TKIP/TKIP) WPA2(PSK/AES,TKIP/TKIP)
                        CFL-6BDJ                   -62  64      Y  -- WPA(PSK/AES,TKIP/TKIP) WPA2(PSK/AES,TKIP/TKIP)
```

On macOS Monterey, `12.0 Beta (21A5522h)`.  `Python 3.6.5 |Anaconda, Inc.| (default, Apr 26 2018, 08:42:37)`

(It output one of those chunks for every SSID in the list but I don't think it's useful to provide all of them?)

Comments:

Patch:
diff --git a/access_points/__init__.py b/access_points/__init__.py
index b0040cf..ab7ecdc 100644
--- a/access_points/__init__.py
+++ b/access_points/__init__.py
@@ -94,22 +94,28 @@ class OSXWifiScanner(WifiScanner):
         cmd = "airport -s"
         return path + cmd
 
+    # OSX Monterey doesn't output the BSSID unless you `sudo` which means the
+    # old method using a regexp to match those lines fails.  Since the output
+    # is column-formatted, we can use that instead and it works on both
+    # Monterey-without-BSSID and pre-Monterey-with-BSSID.
     def parse_output(self, output):
         results = []
-        # 5 times 2 "letters and/or digits" followed by ":"
-        # Then one time only 2 "letters and/or digits"
-        # Use non-capturing groups (?:...) to use {} for amount
-        # One wrapping group (...) to capture the whole thing
-        bbsid_re = re.compile("((?:[0-9a-zA-Z]{2}:){5}(?:[0-9a-zA-Z]){2})")
         security_start_index = False
+        # First line looks like this (multiple whitespace truncated to fit.)
+        # `\w+SSID BSSID\w+  RSSI CHANNEL HT CC SECURITY (auth/unicast/group)`
+        # `       ^ ssid_end_index`
+        # `                  ^ rssi_start_index`
+        # `        ^       ^ bssid`
         for line in output.split("\n"):
             if line.strip().startswith("SSID BSSID"):
                 security_start_index = line.index("SECURITY")
+                ssid_end_index = line.index("SSID") + 4
+                rssi_start_index = line.index("RSSI")
             elif line and security_start_index and 'IBSS' not in line:
                 try:
-                    ssid = bbsid_re.split(line)[0].strip()
-                    bssid = bbsid_re.findall(line)[0]
-                    rssi = bbsid_re.split(line)[-1].strip().split()[0]
+                    ssid = line[0:ssid_end_index].strip()
+                    bssid = line[ssid_end_index+1:rssi_start_index-1].strip()
+                    rssi = line[rssi_start_index:rssi_start_index+4].strip()
                     security = line[security_start_index:]
                     ap = AccessPoint(ssid, bssid, rssi_to_quality(int(rssi)), security)
                     results.append(ap)


Test patch:
diff --git a/data/osx_monterey_test.txt b/data/osx_monterey_test.txt
new file mode 100644
index 0000000..a10d443
--- /dev/null
+++ b/data/osx_monterey_test.txt
@@ -0,0 +1,6 @@
+                            SSID BSSID             RSSI CHANNEL HT CC SECURITY (auth/unicast/group)
+                     X000X000X00                   -83  6       Y  -- WPA(PSK/AES,TKIP/TKIP) WPA2(PSK/AES,TKIP/TKIP)
+               XXX-XXX0000000000                   -68  5       Y  -- WPA(PSK/TKIP/TKIP) WPA2(PSK/AES/TKIP)
+                       XXXXXXXXX                   -52  8       N  -- WPA(PSK/TKIP/TKIP)
+                          XX-XXX                   -75  10      Y  -- WPA(PSK/TKIP/TKIP) WPA2(PSK/AES/TKIP)
+                   XXXXXXX00X0X0                   -58  10      N  -- WPA(PSK/TKIP/TKIP) WPA2(PSK/AES/TKIP)
diff --git a/tests/all_test.py b/tests/all_test.py
index d4b6960..0fba97a 100644
--- a/tests/all_test.py
+++ b/tests/all_test.py
@@ -26,18 +26,20 @@ def read_output(fn):
         return f.read()
 
 
-def assert_access_point(aps):
+def assert_access_point(aps, bssid_required=True):
     assert isinstance(aps, list)
     for ap in aps:
         assert isinstance(ap['quality'], int)
         assert isinstance(ap['ssid'], basestring) and ap['ssid'] != ''
-        assert isinstance(ap['bssid'], basestring) and ap['bssid'] != ''
+        # `ap['bssid']` can sometimes be empty, e.g. on macOS Monterey
+        if bssid_required:
+            assert isinstance(ap['bssid'], basestring) and ap['bssid'] != ''
 
 
-def parse_output(wifi_scanner, fname):
+def parse_output(wifi_scanner, fname, bssid_required=True):
     output = read_output(fname)
     aps = wifi_scanner.parse_output(output)
-    assert_access_point(aps)
+    assert_access_point(aps, bssid_required)
     return aps
 
 
@@ -53,7 +55,9 @@ def assert_all_included(aps, answers):
 def test_scan():
     scanner = get_scanner()
     aps = scanner.get_access_points()
-    assert_access_point(aps)
+    # We don't know if we necessarily get BSSIDs from a live scan;
+    # best to err on the side of caution here and not require a match.
+    assert_access_point(aps, False)
 
 
 def test_iwlist():
@@ -249,6 +253,35 @@ def test_osx():
     ]
     assert_all_included(aps, osx_ans)
 
+def test_osx_monterey():
+    # BSSID isn't a required match for macOS Monterey because it's not there.
+    aps = parse_output(OSXWifiScanner(), "osx_monterey_test.txt", False)
+    assert len(aps) == 5
+
+    osx_monterey_ans = [
+        ('X000X000X00',
+         '',
+         rssi_to_quality(-83),
+         'WPA(PSK/AES,TKIP/TKIP) WPA2(PSK/AES,TKIP/TKIP)'),
+        ('XXX-XXX0000000000',
+         '',
+         rssi_to_quality(-68),
+         'WPA(PSK/TKIP/TKIP) WPA2(PSK/AES/TKIP)'),
+        ('XXXXXXXXX',
+         '',
+         rssi_to_quality(-52),
+         'WPA(PSK/TKIP/TKIP)'),
+        ('XX-XXX',
+         '',
+         rssi_to_quality(-75),
+         'WPA(PSK/TKIP/TKIP) WPA2(PSK/AES/TKIP)'),
+        ('XXXXXXX00X0X0',
+         '',
+         rssi_to_quality(-58),
+         'WPA(PSK/TKIP/TKIP) WPA2(PSK/AES/TKIP)')
+    ]
+    assert_all_included(aps, osx_monterey_ans)
+
 
 def test_termux():
     aps = parse_output(TermuxWifiScanner(), "termux_test.txt")