10b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# Copyright (C) 2010 The Android Open Source Project
20b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
30b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# Licensed under the Apache License, Version 2.0 (the "License");
40b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# you may not use this file except in compliance with the License.
50b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# You may obtain a copy of the License at
60b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
70b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#      http://www.apache.org/licenses/LICENSE-2.0
80b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
90b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# Unless required by applicable law or agreed to in writing, software
100b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# distributed under the License is distributed on an "AS IS" BASIS,
110b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
120b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# See the License for the specific language governing permissions and
130b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# limitations under the License.
140b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
150b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# A nawk/gawk script used to extract the list of launchable activities
160b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# from an application's manifest (i.e. AndroidManifest.xml). Usage:
170b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
180b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#   awk -f <this-script> AndroidManifest.xml
190b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
200b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner
210b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
220b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# Explanation:
230b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
240b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# A given application can have several activities, and each activity
250b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# can have several intent filters. We want to only list, in the final
260b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# output, the activities which have a intent-filter that contains the
270b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# following elements:
280b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
290b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#   <action android:name="android.intent.action.MAIN" />
300b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#   <category android:name="android.intent.category.LAUNCHER" />
310b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
320b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# To do this, we need hooks called when entering and exiting <activity>
330b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# and <intent-filter> elements.
340b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
350b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner
360b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' TurnerBEGIN {
370b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    while ( xml_event() ) {
380b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # concat xml event type and tag for simpler comparisons
390b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        event = XML_TYPE "-" XML_TAG;
400b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # When entering a new <activity>, extract its name and set
410b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # the 'launchable' flag to false.
420b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        if ( event == "BEGIN-ACTIVITY" && 
430b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner             XML_RPATH == "ACTIVITY/APPLICATION/MANIFEST/" ) {
440b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            name = XML_ATTR["android:name"];
450b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            launchable = 0;
460b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
470b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # When exiting an <activity>, check that it has a name and
480b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # is launchable. If so, print its name to the output
490b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        else if ( event == "END-ACTIVITY" &&
500b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                  XML_RPATH == "APPLICATION/MANIFEST/" ) {
510b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            if ( name && launchable ) {
528c3d67c878fac8a5bef0458ef19ac06d959c7488David 'Digit' Turner                # If the name doesn't contain any dot, we consider
538c3d67c878fac8a5bef0458ef19ac06d959c7488David 'Digit' Turner                # that it is just missing the initial one.
548c3d67c878fac8a5bef0458ef19ac06d959c7488David 'Digit' Turner                if (index(name, ".") == 0) {
558c3d67c878fac8a5bef0458ef19ac06d959c7488David 'Digit' Turner                    name = "." name
568c3d67c878fac8a5bef0458ef19ac06d959c7488David 'Digit' Turner                }
570b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                print name;
580b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            }
590b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
600b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # When entering an <intent-filter> inside an <activity>, clear
610b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # the 'action' and 'category' variables. They are updated when
620b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # we enter the corresponding elements within the intent-filter.
630b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        else if ( event == "BEGIN-INTENT-FILTER" &&
640b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                 XML_RPATH == "INTENT-FILTER/ACTIVITY/APPLICATION/MANIFEST/" ) {
65b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner            action_main = 0;
66b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner            category_launcher = 0;
670b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
680b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # When exiting an <intent-filter>, set the 'launchable' flag to true
690b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # for the current activity if both 'action' and 'category' have the
700b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # correct name.
710b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        else if ( event == "END-INTENT-FILTER" &&
720b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                  XML_RPATH == "ACTIVITY/APPLICATION/MANIFEST/" ) {
73b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner            if ( category_launcher ) {
74b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner                launchable = 1;
750b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            }
760b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
770b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # When entering an <action> element inside an <intent-filter>, record
780b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # its name.
790b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        else if ( event == "BEGIN-ACTION" &&
800b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                  XML_RPATH == "ACTION/INTENT-FILTER/ACTIVITY/APPLICATION/MANIFEST/" ) {
81b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner            action_main = 0;
82b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner            if ( XML_ATTR["android:name"] == "android.intent.action.MAIN" ) {
83b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner                action_main = 1;
84b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner            }
850b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
860b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # When entering a <category> element inside an <intent-filter>, record
870b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # its name.
880b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        else if ( event == "BEGIN-CATEGORY" &&
890b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                  XML_RPATH == "CATEGORY/INTENT-FILTER/ACTIVITY/APPLICATION/MANIFEST/" ) {
90b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner            if ( action_main && XML_ATTR["android:name"] == "android.intent.category.LAUNCHER" ) {
91b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner                category_launcher = 1;
92b2d2128c1cd0f92455b201bca17897ecf07b48d0David 'Digit' Turner            }
930b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
940b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    }
950b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner}
960b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner
970b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner
980b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
990b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# the following is copied directly from xml.awk - see this file for
1000b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner# usage and implementation details.
1010b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner#
1020b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turnerfunction xml_event () {
1030b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    RS=">";
1040b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    XML_TAG=XML_TYPE="";
1050b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    split("", XML_ATTR);
1060b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    while ( 1 ) {
1070b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        if (_xml_closing) { # delayed direct tag closure
1080b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            XML_TAG = _xml_closing;
1090b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            XML_TYPE = "END";
1100b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_closing = "";
1110b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_exit(XML_TAG);
1120b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            return 1;
1130b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
1140b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        if (getline <= 0) return 0; # read new input line
1150b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        _xml_p = index($0, "<"); # get start marker
1160b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        if (_xml_p == 0) return 0; # end of file (or malformed input)
1170b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        $0 = substr($0, _xml_p) # remove anything before '<'
1180b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        # ignore CData / Comments / Processing instructions / Declarations
1190b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        if (_xml_in_section("<!\\[[Cc][Dd][Aa][Tt][Aa]\\[", "]]") ||
1200b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_in_section("<!--", "--") ||
1210b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_in_section("<\\?", "\\?") ||
1220b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_in_section("<!", "")) {
1230b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            continue;
1240b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
1250b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        if (substr($0, 1, 2) == "</") { # is it a closing tag ?
1260b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            XML_TYPE = "END";
1270b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            $0 = substr($0, 3);
1280b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        } else { # nope, it's an opening one
1290b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            XML_TYPE = "BEGIN";
1300b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            $0 = substr($0, 2);
1310b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
1320b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        XML_TAG = $0
1330b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        sub("[ \n\t/].*$", "", XML_TAG);  # extract tag name
1340b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        XML_TAG = toupper(XML_TAG);       # uppercase it
1350b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        if ( XML_TAG !~ /^[A-Z][-+_.:0-9A-Z]*$/ )  # validate it
1360b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_panic("Invalid tag name: " XML_TAG);
1370b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        if (XML_TYPE == "BEGIN") {  # update reverse path
1380b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_enter(XML_TAG);
1390b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        } else {
1400b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_exit(XML_TAG);
1410b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
1420b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        sub("[^ \n\t]*[ \n\t]*", "", $0); # get rid of tag and spaces
1430b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        while ($0) { # process attributes
1440b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            if ($0 == "/") {  # deal with direct closing tag, e.g. </foo>
1450b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                _xml_closing = XML_TAG; # record delayed tag closure.
1460b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                break
1470b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            }
1480b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_attrib = $0;
1490b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            sub(/=.*$/,"",_xml_attrib);  # extract attribute name
1500b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            sub(/^[^=]*/,"",$0);         # remove it from record
1510b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            _xml_attrib = tolower(_xml_attrib);
1520b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            if ( _xml_attrib !~ /^[a-z][-+_0-9a-z:]*$/ ) # validate it
1530b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                _xml_panic("Invalid attribute name: " _xml_attrib);
1540b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            if (substr($0,1,2) == "=\"") { # value is ="something"
1550b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                _xml_value = substr($0,3);
1560b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                sub(/".*$/,"",_xml_value);
1570b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                sub(/^="[^"]*"/,"",$0);
1580b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            } else if (substr($0,1,2) == "='") { # value is ='something'
1590b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                _xml_value = substr($0,3);
1600b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                sub(/'.*$/,"",_xml_value);
1610b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                sub(/^='[^']*'/,"",$0);
1620b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            } else {
1630b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner                _xml_panic("Invalid attribute value syntax for " _xml_attrib ": " $0);
1640b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            }
1650b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            XML_ATTR[_xml_attrib] = _xml_value;  # store attribute name/value
1660b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner            sub(/^[ \t\n]*/,"",$0); # get rid of remaining leading spaces
1670b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        }
1680b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        return 1; # now return, XML_TYPE/TAG/ATTR/RPATH are set
1690b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    }
1700b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner}
1710b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner
1720b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turnerfunction _xml_panic (msg) {
1730b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    print msg > "/dev/stderr"
1740b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    exit(1)
1750b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner}
1760b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner
1770b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turnerfunction _xml_in_section (sec_begin, sec_end) {
1780b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    if (!match( $0, "^" sec_begin )) return 0;
1790b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    while (!match($0, sec_end "$")) {
1800b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        if (getline <= 0) _xml_panic("Unexpected EOF: " ERRNO);
1810b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    }
1820b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    return 1;
1830b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner}
1840b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner
1850b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turnerfunction _xml_enter (tag) {
1860b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    XML_RPATH = tag "/" XML_RPATH;
1870b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner}
1880b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner
1890b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turnerfunction _xml_exit (tag) {
1900b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    _xml_p = index(XML_RPATH, "/");
1910b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    _xml_expected = substr(XML_RPATH, 1, _xml_p-1);
1920b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    if (_xml_expected != XML_TAG)
1930b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner        _xml_panic("Unexpected close tag: " XML_TAG ", expecting " _xml_expected);
1940b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner    XML_RPATH = substr(XML_RPATH, _xml_p+1);
1950b2676bac67c271de9989357f6e3b2e762a7adf1David 'Digit' Turner}
196