File size: 5,259 Bytes
9c9a58f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
"""Static-index correctness tests independent of the original payload corpus."""
import hashlib
from pathlib import Path
import tempfile
import unittest

import map_references as mapper


class LexicalMappingTests(unittest.TestCase):
    def test_comments_and_literals_do_not_become_dependencies(self):
        source = '''// #include "fake1.ixpl"
/* C::loadConfigFile("bad.cfg"); */
string x = "C::fake(1)";
string raw = \\"[\n#include "fake2.ixpl"\nC::fake(2);\n]";
#include "Core.ixpl"
namespace C = Core;
void real() { C::loadConfigFile("real.cfg", cfg); }
'''
        report = mapper.scan_ixpl(source)
        self.assertEqual([(x['include'],x['line']) for x in report['includes']], [('Core.ixpl',8)])
        self.assertEqual([(c['qualifier'],c['method']) for c in report['qualified_calls']], [('Core','loadConfigFile')])

    def test_balanced_call_arguments_and_opaque_commas(self):
        source = 'namespace C = Core;\nvoid f() { C::queueTask(id, int[]{1, 2}, nested(3, 4), "x,y"); }'
        call = mapper.scan_ixpl(source)['qualified_calls'][0]
        self.assertEqual(call['arguments'],['id','int[]{1, 2}','nested(3, 4)','"x,y"'])

    def test_interrupt_overloads_keep_signatures_and_source_lines(self):
        source = ('int INT_A = 7;\nvoid get(int &x);\n'
                  'void get(int &x) interrupt(+INT_A, x);\n'
                  'int get() {int x; interrupt(+INT_A, x); return x;}\n')
        wrappers = mapper.scan_ixpl(source)['interrupt_wrappers']
        self.assertEqual([w['wrapper']['signature'] for w in wrappers],['void get(int &x)','int get()'])
        self.assertEqual([w['line'] for w in wrappers],[3,4])
        self.assertEqual(mapper.constant_value(wrappers[0]['arguments'][0],{'INT_A':7}),7)

    def test_historical_struct_return_is_not_attached_to_previous_wrapper(self):
        source = ('type Brush = (int x);\nvoid deselect(int x) interrupt(+1,x);\n'
                  'Brush &read() {Brush b; interrupt(+2,b); return b;}\n')
        wrappers=mapper.scan_ixpl(source)['interrupt_wrappers']
        self.assertEqual(wrappers[1]['wrapper']['name'],'read')
        self.assertEqual(wrappers[1]['wrapper']['line'],3)
        self.assertEqual(wrappers[1]['line'],3)

    def test_numeric_resolution_is_not_code_execution(self):
        self.assertEqual(mapper.constant_value('_INT_BEGIN + 5',{'_INT_BEGIN':249}),254)
        for value in ['__import__("os").system("bad")','X + 1','2 * 3']:
            with self.subTest(value=value), self.assertRaises(ValueError):
                mapper.constant_value(value,{})

    def test_literal_names_resolve_only_unambiguous_bindings(self):
        report = mapper.scan_ixpl('string prefix = "camera.";\nstring name = prefix+"toggle";')
        self.assertEqual(mapper.literal_string('name',report['string_bindings']),'camera.toggle')
        for value in ['missing','"bad"+','call()']:
            self.assertIsNone(mapper.literal_string(value,report['string_bindings']))
        self.assertIsNone(mapper.literal_string('prefix',{'prefix':[{'expression':'"a"'},{'expression':'"b"'}]}))
        self.assertIsNone(mapper.literal_string('cycle',{'cycle':[{'expression':'cycle'}]}))

    def test_large_multiline_literal_is_opaque(self):
        source = 'string data = \\"[' + (' ' * 1000 + '\n') * 80 + ']";\nvoid f() { Core::undo(); }'
        self.assertEqual(mapper.scan_ixpl(source)['qualified_calls'][0]['method'],'undo')
        for source in ['/* unterminated','string s = "bad','string s = \\"[bad']:
            with self.assertRaises(ValueError):mapper.lex(source)

    def test_build_graph_reports_missing_and_ambiguous_candidates(self):
        with tempfile.TemporaryDirectory() as temp:
            root = Path(temp).resolve()
            content = {'tools/includePaths.txt':b'..\\inc1\n..\\inc2\n',
                       'inc1/Core.ixpl':b'namespace Core {}', 'inc2/Core.ixpl':b'namespace Core {}',
                       'data/sources/plugins/tool.ixpl':b'#include "Core.ixpl"\n#include "Missing.ixpl"\n'}
            inventory = {'build':'synthetic','archive_sha256':'a'*64,'files':[]}
            for name,raw in content.items():
                path=root/name;path.parent.mkdir(parents=True,exist_ok=True);path.write_bytes(raw)
                inventory['files'].append({'path':name,'sha256':hashlib.sha256(raw).hexdigest()})
            build = mapper.index_build(root,inventory)
            plugin = next(f for f in build['files'] if f['category']=='plugin')
            self.assertEqual([e['status'] for e in plugin['includes']],['ambiguous','missing'])
            (root/'inc1/Core.ixpl').write_bytes(b'changed')
            with self.assertRaises(ValueError):mapper.index_build(root,inventory)

    def test_diffs_distinguish_renames_copies_and_changes(self):
        old={'build':'a','files':[{'path':'old.txt','sha256':'1'},{'path':'retained.txt','sha256':'2'}]}
        new={'build':'b','files':[{'path':'new.txt','sha256':'1'},{'path':'retained.txt','sha256':'2'}, {'path':'copy.txt','sha256':'2'}]}
        diff=mapper.differences([old,new])[0]
        self.assertEqual(diff['identical_rename_candidates'],[{'from':'old.txt','to':'new.txt'}])
        self.assertEqual(diff['identical_copy_candidates'],[{'from':'retained.txt','to':'copy.txt'}])