XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
"""Source-bound guest Bing acceptance. This module performs no host HTTP requests."""
import hashlib
import json
import math
from pathlib import Path
import re
import time
from urllib.parse import urljoin, urlsplit

from PIL import Image

RESPONSE = 'XFE_NATIVE_WEB_RESPONSE'
REMOTE = 'XFE_REMOTE_PAGE_PROOF'
HOME = 'https://www.bing.com/'
NAT_ARGUMENT = 'user,id=NativeNet,net=10.0.2.0/24,host=10.0.2.2,dns=10.0.2.3,dhcpstart=10.0.2.15'
NIC_ARGUMENT = 'e1000,netdev=NativeNet'
TITLE = re.compile(r'bing|必应', re.I)
PRESENTED = re.compile(r'^XFE_BROWSER_BACKGROUND_PRESENTED\|Window\|(\d+)\|Generation\|(\d+)\|Address\|([^|\r\n]+)'
                       r'\|SnapshotSequence\|(\d+)\|PresentationSequence\|(\d+)\|PresentationTicks\|(\d+)\|ScrollY\|([\d.]+)\r?$', re.M)
WINDOW = re.compile(r'^XFE_WINDOW_STATE\|(\d+)\|(\w+)\|(-?\d+)\|(-?\d+)\|(\d+)\|(\d+)\|([01])\|([01])\|([01])\r?$', re.M)
BROWSER_STATE = re.compile(r'XFE_BROWSER_STATE_BEGIN\r?\n(\d+)\|([^\r\n]+)\r?\n(.*?)\r?\nXFE_BROWSER_STATE_END', re.S)


def native_network_profile(enabled):
    """Describe the actual QEMU profile, without claiming guest network success."""
    return {'Kind': 'XfeIndependentNativeNetworkProfile', 'Enabled': bool(enabled),
            'Backend': 'QEMU user NAT' if enabled else 'None', 'Device': 'e1000' if enabled else None,
            'PciVendorDevice': '8086:100e' if enabled else None,
            'GuestAddress': '10.0.2.15' if enabled else None, 'Gateway': '10.0.2.2' if enabled else None,
            'DnsServer': '10.0.2.3' if enabled else None, 'AddressConfiguration': 'Static' if enabled else None,
            'NetdevArgument': NAT_ARGUMENT if enabled else None, 'DeviceArgument': NIC_ARGUMENT if enabled else None,
            'HostProxy': False, 'HostForwarding': False, 'Rtc': 'base=utc', 'Cpu': 'max',
            'EntropyPolicy': 'Guest CPUID plus bounded RDSEED/RDRAND with carry check; unavailable fails closed'}


def network_arguments(enabled):
    return ['-netdev', NAT_ARGUMENT, '-device', NIC_ARGUMENT] if enabled else ['-net', 'none']


def validate_network_arguments(arguments, enabled):
    expected = network_arguments(enabled)
    options = ('-net', '-netdev', '-nic', '-device')
    actual = []
    for index, argument in enumerate(arguments):
        if argument not in options:
            continue
        assert index + 1 < len(arguments), 'Missing QEMU network argument value'
        value = arguments[index + 1]
        actual.extend((argument, value))
    assert actual == expected, 'QEMU network arguments differ from the source-bound profile'
    for key, value in (('-cpu', 'max'), ('-rtc', 'base=utc')):
        indices = [index for index, item in enumerate(arguments) if item == key]
        assert len(indices) == 1 and indices[0] + 1 < len(arguments) and arguments[indices[0] + 1] == value, 'Missing or ambiguous CPU/UTC profile'
    return native_network_profile(enabled)


def window_geometry(log, application):
    end = log.rfind('XFE_DESKTOP_STATE_END')
    begin = log.rfind('XFE_DESKTOP_ACTIVE|', 0, end)
    assert begin >= 0 and end > begin, 'No complete current native desktop state'
    matches = [item for item in WINDOW.finditer(log[begin:end]) if item[2] == application and item[7] == '0']
    assert matches, 'No visible actual ' + application + ' window geometry'
    match = matches[-1]
    return int(match[1]), [int(match[index]) for index in (3, 4, 5, 6)]


def json_blocks(log, marker, after=0):
    expression = re.compile(re.escape(marker) + r'_BEGIN\r?\n(.*?)\r?\n' + re.escape(marker) + r'_END', re.S)
    result = []
    for match in expression.finditer(log, after):
        try:
            value = json.loads(match[1])
        except (ValueError, TypeError):
            continue  # An incomplete/rejected packet never counts as evidence.
        if isinstance(value, dict):
            result.append({'Value': value, 'SerialOffset': match.start(), 'EndOffset': match.end(), 'Raw': match[0]})
    return result


def _url(value):
    parsed = urlsplit(value)
    assert parsed.scheme == 'https' and parsed.hostname and not parsed.username and not parsed.password, 'A Bing hop must use HTTPS without credentials'
    assert parsed.port in (None, 443), 'Unexpected HTTPS port'
    return parsed


def _same_url(left, right):
    try:
        a, b = _url(left), _url(right)
    except (AssertionError, TypeError, ValueError, AttributeError):
        return False
    return (a.hostname.lower(), a.path or '/', a.query) == (b.hostname.lower(), b.path or '/', b.query)


def validate_response_chain(packets, initial=HOME):
    """Validate actual guest response packets; redirects cannot become implicit success."""
    expected, chain, seen = initial, [], set()
    for packet in packets:
        value = packet['Value']
        if not _same_url(value.get('RequestUri', ''), expected):
            continue
        uri = _url(value['RequestUri'])
        assert value.get('NativeTransport') is True, 'A host transport cannot qualify'
        assert value.get('CertificateValidated') is True, 'Every HTTPS hop requires validated TLS'
        assert _same_url(value['ResponseUri'], value['RequestUri']), 'The hop response address differs from its request'
        request, headers = value.get('RequestHeaders', ''), value.get('ResponseHeaders', '')
        target = (uri.path or '/') + ('?' + uri.query if uri.query else '')
        assert request.splitlines() and request.splitlines()[0] == 'GET ' + target + ' HTTP/1.1', 'Missing actual HTTP request line'
        host = re.search(r'^Host:\s*([^\r\n]+)', request, re.M | re.I)
        assert host and host[1].strip().lower() in (uri.hostname.lower(), uri.hostname.lower() + ':443'), 'HTTP Host does not match TLS URL'
        status = value.get('StatusCode')
        assert type(status) is int and re.match(r'^HTTP/1\.[01]\s+' + str(status) + r'(?:\s|\r?$)', headers), 'Status differs from raw response headers'
        assert value.get('RequestId') is not None, 'Missing guest request identity'
        chain.append(packet)
        if status in (301, 302, 303, 307, 308):
            location = value.get('Location')
            assert isinstance(location, str) and location, 'Redirect has no Location'
            raw_location = re.search(r'^Location:\s*([^\r\n]+)', headers, re.M | re.I)
            assert raw_location and raw_location[1].strip() == location, 'Redirect differs from raw Location'
            expected = urljoin(value['RequestUri'], location)
            _url(expected)
            assert expected not in seen and len(chain) <= 5, 'Redirect cycle or bound exceeded'
            seen.add(expected)
            continue
        assert status == 200, 'Bing did not return HTTP 200'
        assert uri.hostname.lower() == 'bing.com' or uri.hostname.lower().endswith('.bing.com'), 'The final page is outside Bing'
        assert value.get('ContentType', '').split(';')[0].strip().lower() in ('text/html', 'application/xhtml+xml'), 'The response is not HTML'
        assert value.get('HasHtml') is True and value.get('HasHead') is True, 'No actual HTML/head was received'
        assert TITLE.search(value.get('HtmlTitle', '')), 'The real HTML title is not Bing'
        assert type(value.get('BodyBytes')) is int and value['BodyBytes'] >= 128, 'No bounded actual HTML body'
        assert re.fullmatch(r'[0-9a-fA-F]{64}', value.get('BodySha256', '')), 'Missing actual HTML fingerprint'
        return chain
    raise AssertionError('No complete validated native HTTPS Bing response chain')


def _rect(value):
    values = [float(value[name]) for name in ('X', 'Y', 'Width', 'Height')]
    assert all(math.isfinite(number) for number in values) and values[2] > 0 and values[3] > 0, 'Invalid actual element geometry'
    return values


def validate_remote_page(page, response):
    assert _same_url(page['Address'], response['ResponseUri']), 'Remote DOM belongs to another address'
    assert page.get('BodySha256', '').lower() == response['BodySha256'].lower(), 'Rendered HTML does not match the HTTP body'
    assert page.get('CertificateValidated') is True, 'The actual rendered response must retain validated TLS'
    assert type(page.get('SnapshotSequence')) is int and page['SnapshotSequence'] > 0, 'Missing actual immutable paint snapshot identity'
    assert TITLE.search(page.get('Title', '')) and not re.search(r'failed|unavailable|\berror\b|错误|不可用', page.get('Title', ''), re.I), 'A failure page cannot qualify'
    width, height = float(page['ViewportWidth']), float(page['ViewportHeight'])
    assert width > 0 and height > 0 and math.isfinite(width + height)
    searches = [item for item in page.get('SearchInputs', []) if item.get('Name') == 'q']
    rectangles = [_rect(item['Bounds']) for item in searches]
    assert any(x < width and y < height and x + w > 0 and y + h > 0 for x, y, w, h in rectangles), 'No actual Bing search input geometry'
    assert page.get('TextRuns'), 'No actual remote text geometry'
    loaded = {}
    for item in page.get('LoadedStyles', []):
        if item.get('Characters', 0) <= 0:
            continue
        # A real redirect receipt keeps both the DOM-requested stylesheet URI
        # and its validated final URI. Unrelated final resources cannot stand
        # in for the page's declared request. Older unredirected receipts may
        # use Address alone, without inventing an implicit redirect.
        requested = item.get('RequestedAddress', item['Address'])
        final = item.get('FinalAddress', item['Address'])
        _url(requested); _url(final)
        assert _same_url(item['Address'], final), 'The stylesheet final address differs from its actual resource receipt'
        loaded[requested] = item
    for declared in page.get('DeclaredStyleLinks', []):
        _url(declared)
        assert declared in loaded, 'A declared external stylesheet did not load: ' + declared
    return {'SearchInputs': searches, 'LoadedStyles': list(loaded.values()),
            'InlineStyleCount': page.get('InlineStyleCount'), 'DeclaredStyleLinks': page.get('DeclaredStyleLinks', []),
            'CssParityVerified': False, 'Meaning': 'Actual resource receipts and element geometry; full Windows CSS/JS parity is not inferred.'}


def native_navigation_failure(serial, after, window_id):
    """Only a fresh complete state for the current native Bing window qualifies."""
    matches = [item for item in BROWSER_STATE.finditer(serial, after) if int(item[1]) == window_id]
    if not matches:
        return None
    item = matches[-1]
    try:
        address = _url(item[2])
    except (AssertionError, TypeError, ValueError):
        return None
    if address.hostname.lower() != 'bing.com' and not address.hostname.lower().endswith('.bing.com'):
        return None
    content = item[3].strip()
    if not content.startswith('Navigation failed:'):
        return None
    return {'WindowId': window_id, 'Address': item[2], 'Text': content,
            'SerialOffset': item.start(), 'Raw': item[0]}


def matching_presentation(serial, page, window_id, after=0):
    """The latest paint for this window must be this exact immutable DOM snapshot."""
    matches = [item for item in PRESENTED.finditer(serial, after) if int(item[1]) == window_id]
    assert matches, 'No actual native browser presentation'
    item = matches[-1]
    assert _same_url(item[3], page['Address']) and int(item[4]) == page['SnapshotSequence'], 'The latest presented browser snapshot differs from the remote DOM'
    assert int(item[5]) > 0 and int(item[6]) > 0, 'No actual scanout presentation identity/time'
    assert abs(float(item[7]) - float(page.get('ScrollY', 0))) <= .001, 'Rendered remote scroll position differs'
    return item


def presentation_identity(item):
    return {'WindowId': int(item[1]), 'Generation': int(item[2]), 'Address': item[3],
            'SnapshotSequence': int(item[4]), 'PresentationSequence': int(item[5]),
            'PresentationTicks': int(item[6]), 'ScrollY': float(item[7])}


def stable_screenshot_presentation(before, after):
    for name in ('WindowId', 'Generation', 'Address', 'SnapshotSequence', 'ScrollY'):
        assert before[name] == after[name], 'Browser presentation changed during screenshot: ' + name
    assert after['PresentationSequence'] >= before['PresentationSequence'] and after['PresentationTicks'] >= before['PresentationTicks'], 'Browser presentation counters regressed during screenshot'
    return {'Before': before, 'After': after, 'StableSnapshot': True,
            'Meaning': 'Actual QMP framebuffer capture bracketed by the same immutable native browser snapshot; this is not physical display timing.'}


def _color(value):
    value = value.strip().lower()
    if re.fullmatch(r'#?[0-9a-f]{6}', value):
        value = value.lstrip('#')
        return tuple(int(value[index:index+2], 16) for index in (0, 2, 4))
    match = re.fullmatch(r'rgba?\(\s*(\d+(?:\.\d+)?)\s*[, ]\s*(\d+(?:\.\d+)?)\s*[, ]\s*(\d+(?:\.\d+)?)(?:\s*[,/]\s*[\d.]+)?\s*\)', value)
    assert match, 'Unsupported diagnostic text color: ' + value
    return tuple(round(float(number)) for number in match.groups())


def remote_text_pixels(frame, page, viewport, scale=1):
    """Require nonuniform ink at visible actual text geometry in the guest PNG."""
    image = frame.convert('RGB'); samples = []
    for run in page.get('TextRuns', [])[:128]:
        if len(run.get('Text', '').strip()) < 2:
            continue
        x, y, width, height = _rect(run['Bounds'])
        left, top = max(0, x), max(0, y)
        right, bottom = min(viewport[2], x + width), min(viewport[3], y + height)
        if right <= left or bottom <= top:
            continue
        bounds = [round((viewport[0] + left) * scale), round((viewport[1] + top) * scale),
                  round((viewport[0] + right) * scale), round((viewport[1] + bottom) * scale)]
        region = image.crop(bounds); color = _color(run['Color'])
        ink = [(px, py) for py in range(region.height) for px in range(region.width)
               if all(abs(region.getpixel((px, py))[channel] - color[channel]) <= 14 for channel in range(3))]
        area = region.width * region.height
        columns = len({point[0] for point in ink}); rows = len({point[1] for point in ink})
        passed = len(ink) >= 12 and columns >= 6 and rows >= 3 and len(ink) < area * .75
        samples.append({'Text': run['Text'][:96], 'NativeBounds': bounds, 'InkPixels': len(ink), 'InkColumns': columns, 'InkRows': rows, 'Passed': passed})
    return {'Passed': sum(item['Passed'] for item in samples) >= 2, 'Samples': samples,
            'Meaning': 'Nonuniform text ink inside real DOM rectangles in guest framebuffer PNG; no host browser image or synthetic acceptance pixels.'}


def run_bing(proof_directory, log, healthy, wait, screenshot, key, type_text, move, *, timeout=180, open_browser=True, start_offset=None):
    """Open the product's default Browser through QMP. Caller owns QEMU and cleanup."""
    proof_directory = Path(proof_directory)
    started = time.monotonic(); initial_log = healthy()
    assert open_browser or type(start_offset) is int, 'An already-open browser requires the actual input start offset'
    after = len(initial_log) if start_offset is None else start_offset
    assert 0 <= after <= len(initial_log), 'Invalid actual navigation evidence offset'
    result = {'Kind': 'XfeIndependentNativeBingEvidence', 'Passed': False, 'AllUserRequirementsMet': False,
              'NativeHttpsBingLoaded': False, 'FullCssParityVerified': False, 'CompleteJavaScriptCompatibility': False,
              'HostHttpRequests': False, 'HostBrowserScreenshots': False, 'HostJavaScriptInjection': False,
              'VmStartedByThisTool': False, 'Input': 'Actual QMP keyboard/mouse only', 'RequestedUrl': HOME,
              'TimeoutSeconds': timeout, 'Screenshots': []}
    output = proof_directory / 'BingEvidence.json'

    def persist():
        result['ElapsedHostSeconds'] = time.monotonic() - started
        output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')

    persist()
    try:
        # The desktop profile starts with its menu closed. Preserve the product
        # default navigation: do not replace it with an offline/failure fixture.
        if open_browser:
            key('ctrl-esc'); key('down'); key('down'); key('ret')
        wait('XFE_DESKTOP_ACTIVE|Browser', after)
        end = time.monotonic() + timeout
        last_error = 'No native response packet'
        accepted = None
        while time.monotonic() < end:
            serial = healthy()
            try:
                window_id, _ = window_geometry(serial, 'Browser')
            except AssertionError:
                window_id = None  # The complete desktop-state packet may still be arriving.
            failure = native_navigation_failure(serial, after, window_id) if window_id is not None else None
            if failure is not None:
                result['NavigationFailure'] = {key: value for key, value in failure.items() if key != 'Raw'}
                raise AssertionError('Actual native Bing navigation failed: ' + failure['Text'])
            try:
                packets = json_blocks(serial, RESPONSE, after)
                chain = validate_response_chain(packets)
                final = chain[-1]['Value']
                remote_packets = json_blocks(serial, REMOTE, after)
                for remote in reversed(remote_packets):
                    page = remote['Value']
                    css = validate_remote_page(page, final)
                    window_id, bounds = window_geometry(serial, 'Browser')
                    presented = matching_presentation(serial, page, window_id, after)
                    accepted = (chain, remote, css, presented, bounds)
                    break
                assert accepted, 'No matching rendered remote DOM readback'
                break
            except (AssertionError, KeyError, TypeError, ValueError) as error:
                last_error = str(error)
            time.sleep(.1)
        assert accepted, 'Actual native Bing acceptance timed out: ' + last_error
        chain, remote, css, presented, bounds = accepted
        result['Responses'] = [packet['Value'] for packet in chain]
        result['RemotePage'] = remote['Value']; result['CssResources'] = css
        result['Presentation'] = presentation_identity(presented)
        viewport = [bounds[0] + 2, bounds[1] + 67, bounds[2] - 4, bounds[3] - 96]
        assert abs(viewport[2] - float(remote['Value']['ViewportWidth'])) <= 1 and abs(viewport[3] - float(remote['Value']['ViewportHeight'])) <= 1
        move(1200, 710)
        for attempt in range(1, 5):
            serial = healthy(); window_id, current_bounds = window_geometry(serial, 'Browser')
            assert current_bounds == bounds, 'The browser geometry changed before its actual screenshot'
            before = presentation_identity(matching_presentation(serial, remote['Value'], window_id, after))
            name = 'Browser-Bing-Native-' + str(attempt)
            screenshot(name)
            serial = healthy(); current_id, current_bounds = window_geometry(serial, 'Browser')
            assert current_id == window_id and current_bounds == bounds, 'The browser geometry changed during its actual screenshot'
            presentation = stable_screenshot_presentation(before,
                presentation_identity(matching_presentation(serial, remote['Value'], window_id, after)))
            path = proof_directory / (name + '.png')
            with Image.open(path) as frame:
                assert frame.width >= viewport[0] + viewport[2] and frame.height >= viewport[1] + viewport[3]
                ink = remote_text_pixels(frame, remote['Value'], viewport)
            result['Screenshots'].append({'Path': path.name, 'Sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
                'BodySha256': remote['Value']['BodySha256'], 'PresentationCorrelation': presentation, 'TextPixels': ink})
            if ink['Passed']:
                break
            time.sleep(.25)
        assert result['Screenshots'][-1]['TextPixels']['Passed'], 'The actual Bing text was not painted at its remote DOM geometry'
        result.update(Passed=True, NativeHttpsBingLoaded=True,
                      Meaning='Validated guest HTTPS 200, real Bing HTML/DOM, actual native presentation and framebuffer screenshot; full CSS/JS parity remains unverified.')
    except Exception as error:
        result['Failure'] = str(error)
        raise
    finally:
        # Raw packets survive timeout and partial failure, without dumping the
        # entire HTML body or making another network request on the host.
        try:
            serial = log()
            packets = json_blocks(serial, RESPONSE, after) + json_blocks(serial, REMOTE, after)
            raw = proof_directory / 'BingRequestEvidence.log'
            presented_packets = [{'Raw': match[0], 'SerialOffset': match.start()} for match in PRESENTED.finditer(serial, after)]
            failure_packets = [{'Raw': match[0], 'SerialOffset': match.start()} for match in BROWSER_STATE.finditer(serial, after)
                               if match[3].strip().startswith('Navigation failed:')]
            raw.write_text('\n'.join(packet['Raw'] for packet in sorted(packets + presented_packets + failure_packets, key=lambda item: item['SerialOffset'])), encoding='utf-8')
            result['RawGuestEvidence'] = {'Path': raw.name, 'Sha256': hashlib.sha256(raw.read_bytes()).hexdigest(), 'Packets': len(packets)}
            result['RawGuestEvidence']['PresentationMarkers'] = len(presented_packets)
            result['RawGuestEvidence']['NavigationFailures'] = len(failure_packets)
        except Exception as error:
            result['RawGuestEvidenceFailure'] = str(error)
            result['Passed'] = False
        persist()
    return result