• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

i-net-software / JWebAssembly / 529

pending completion
529

push

travis-ci-com

Horcrux7
fix Unsafe for array elements

12 of 12 new or added lines in 1 file covered. (100.0%)

5925 of 6780 relevant lines covered (87.39%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.9
/src/de/inetsoftware/jwebassembly/watparser/WatParser.java
1
/*
2
   Copyright 2018 - 2023 Volker Berlin (i-net software)
3

4
   Licensed under the Apache License, Version 2.0 (the "License");
5
   you may not use this file except in compliance with the License.
6
   You may obtain a copy of the License at
7

8
       http://www.apache.org/licenses/LICENSE-2.0
9

10
   Unless required by applicable law or agreed to in writing, software
11
   distributed under the License is distributed on an "AS IS" BASIS,
12
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
   See the License for the specific language governing permissions and
14
   limitations under the License.
15

16
*/
17
package de.inetsoftware.jwebassembly.watparser;
18

19
import java.util.ArrayList;
20
import java.util.Iterator;
21
import java.util.List;
22

23
import javax.annotation.Nonnegative;
24
import javax.annotation.Nonnull;
25
import javax.annotation.Nullable;
26

27
import de.inetsoftware.classparser.MethodInfo;
28
import de.inetsoftware.jwebassembly.WasmException;
29
import de.inetsoftware.jwebassembly.module.FunctionName;
30
import de.inetsoftware.jwebassembly.module.ValueTypeConvertion;
31
import de.inetsoftware.jwebassembly.module.WasmCodeBuilder;
32
import de.inetsoftware.jwebassembly.wasm.AnyType;
33
import de.inetsoftware.jwebassembly.wasm.ArrayOperator;
34
import de.inetsoftware.jwebassembly.wasm.ArrayType;
35
import de.inetsoftware.jwebassembly.wasm.MemoryOperator;
36
import de.inetsoftware.jwebassembly.wasm.NamedStorageType;
37
import de.inetsoftware.jwebassembly.wasm.NumericOperator;
38
import de.inetsoftware.jwebassembly.wasm.StructOperator;
39
import de.inetsoftware.jwebassembly.wasm.ValueType;
40
import de.inetsoftware.jwebassembly.wasm.VariableOperator;
41
import de.inetsoftware.jwebassembly.wasm.WasmBlockOperator;
42

43
/**
44
 * Parser for text format of a function.
45
 * 
46
 * @author Volker Berlin
47
 */
48
public class WatParser extends WasmCodeBuilder {
1✔
49

50
    /**
51
     * Parse the given wasm text format and generate a list of WasmInstuctions
52
     * 
53
     * @param wat
54
     *            the text format content of a function
55
     * @param method
56
     *            the method with signature as fallback for a missing variable table
57
     * @param signature
58
     *            alternative for method signature, can be null if method is set
59
     * @param lineNumber
60
     *            the line number for an error message
61
     */
62
    public void parse( String wat, MethodInfo method, Iterator<AnyType> signature, int lineNumber ) {
63
        try {
64
            reset( null, method, signature );
1✔
65

66
            List<String> tokens = splitTokens( wat );
1✔
67
            for( int i = 0; i < tokens.size(); i++ ) {
1✔
68
                int javaCodePos = i;
1✔
69
                String tok = tokens.get( i );
1✔
70
                switch( tok ) {
1✔
71
                    case "local.get":
72
                        addLocalInstruction( VariableOperator.get, getInt( tokens, ++i), javaCodePos, lineNumber );
1✔
73
                        break;
1✔
74
                    case "local.set":
75
                        addLocalInstruction( VariableOperator.set, getInt( tokens, ++i), javaCodePos, lineNumber );
1✔
76
                        break;
1✔
77
                    case "local.tee":
78
                        addLocalInstruction( VariableOperator.tee, getInt( tokens, ++i), javaCodePos, lineNumber );
1✔
79
                        break;
1✔
80
                    case "global.get":
81
                        addGlobalInstruction( true, get( tokens, ++i), javaCodePos, lineNumber );
×
82
                        break;
×
83
                    case "global.set":
84
                        addGlobalInstruction( false, get( tokens, ++i), javaCodePos, lineNumber );
1✔
85
                        break;
1✔
86
                    case "i32.const":
87
                        addConstInstruction( getInt( tokens, ++i), ValueType.i32, javaCodePos, lineNumber );
1✔
88
                        break;
1✔
89
                    case "i32.add":
90
                        addNumericInstruction( NumericOperator.add, ValueType.i32, javaCodePos, lineNumber );
1✔
91
                        break;
1✔
92
                    case "i32.eq":
93
                        addNumericInstruction( NumericOperator.eq, ValueType.i32, javaCodePos, lineNumber );
1✔
94
                        break;
1✔
95
                    case "i32.div_s":
96
                        addNumericInstruction( NumericOperator.div, ValueType.i32, javaCodePos, lineNumber );
1✔
97
                        break;
1✔
98
                    case "i32.eqz":
99
                        addNumericInstruction( NumericOperator.eqz, ValueType.i32, javaCodePos, lineNumber );
1✔
100
                        break;
1✔
101
                    case "i32.mul":
102
                        addNumericInstruction( NumericOperator.mul, ValueType.i32, javaCodePos, lineNumber );
1✔
103
                        break;
1✔
104
                    case "i32.ne":
105
                        addNumericInstruction( NumericOperator.ne, ValueType.i32, javaCodePos, lineNumber );
1✔
106
                        break;
1✔
107
                    case "i32.reinterpret_f32":
108
                        addConvertInstruction( ValueTypeConvertion.f2i_re, javaCodePos, lineNumber );
1✔
109
                        break;
1✔
110
                    case "i32.trunc_sat_f32_s":
111
                        addConvertInstruction( ValueTypeConvertion.f2i, javaCodePos, lineNumber );
1✔
112
                        break;
1✔
113
                    case "i32.wrap_i64":
114
                        addConvertInstruction( ValueTypeConvertion.l2i, javaCodePos, lineNumber );
×
115
                        break;
×
116
                    case "i64.const":
117
                        addConstInstruction( Long.parseLong( get( tokens, ++i ) ), ValueType.i64, javaCodePos, lineNumber );
1✔
118
                        break;
1✔
119
                    case "i64.add":
120
                        addNumericInstruction( NumericOperator.add, ValueType.i64, javaCodePos, lineNumber );
1✔
121
                        break;
1✔
122
                    case "i64.eq":
123
                        addNumericInstruction( NumericOperator.eq, ValueType.i64, javaCodePos, lineNumber );
1✔
124
                        break;
1✔
125
                    case "i64.div_s":
126
                        addNumericInstruction( NumericOperator.div, ValueType.i64, javaCodePos, lineNumber );
1✔
127
                        break;
1✔
128
                    case "i64.eqz":
129
                        addNumericInstruction( NumericOperator.eqz, ValueType.i64, javaCodePos, lineNumber );
1✔
130
                        break;
1✔
131
                    case "i64.extend_i32_s":
132
                        addConvertInstruction( ValueTypeConvertion.i2l, javaCodePos, lineNumber );
1✔
133
                        break;
1✔
134
                    case "i64.reinterpret_f64":
135
                        addConvertInstruction( ValueTypeConvertion.d2l_re, javaCodePos, lineNumber );
1✔
136
                        break;
1✔
137
                    case "i64.trunc_sat_f64_s":
138
                        addConvertInstruction( ValueTypeConvertion.d2l, javaCodePos, lineNumber );
1✔
139
                        break;
1✔
140
                    case "f32.abs":
141
                        addNumericInstruction( NumericOperator.abs, ValueType.f32, javaCodePos, lineNumber );
1✔
142
                        break;
1✔
143
                    case "f32.ceil":
144
                        addNumericInstruction( NumericOperator.ceil, ValueType.f32, javaCodePos, lineNumber );
1✔
145
                        break;
1✔
146
                    case "f32.const":
147
                        addConstInstruction( Float.parseFloat( get( tokens, ++i ) ), ValueType.f32, javaCodePos, lineNumber );
1✔
148
                        break;
1✔
149
                    case "f32.convert_i32_s":
150
                        addConvertInstruction( ValueTypeConvertion.i2f, javaCodePos, lineNumber );
1✔
151
                        break;
1✔
152
                    case "f32.div":
153
                        addNumericInstruction( NumericOperator.div, ValueType.f32, javaCodePos, lineNumber );
1✔
154
                        break;
1✔
155
                    case "f32.floor":
156
                        addNumericInstruction( NumericOperator.floor, ValueType.f32, javaCodePos, lineNumber );
1✔
157
                        break;
1✔
158
                    case "f32.max":
159
                        addNumericInstruction( NumericOperator.max, ValueType.f32, javaCodePos, lineNumber );
1✔
160
                        break;
1✔
161
                    case "f32.min":
162
                        addNumericInstruction( NumericOperator.min, ValueType.f32, javaCodePos, lineNumber );
1✔
163
                        break;
1✔
164
                    case "f32.mul":
165
                        addNumericInstruction( NumericOperator.mul, ValueType.f32, javaCodePos, lineNumber );
1✔
166
                        break;
1✔
167
                    case "f32.nearest":
168
                        addNumericInstruction( NumericOperator.nearest, ValueType.f32, javaCodePos, lineNumber );
1✔
169
                        break;
1✔
170
                    case "f32.reinterpret_i32":
171
                        addConvertInstruction( ValueTypeConvertion.i2f_re, javaCodePos, lineNumber );
1✔
172
                        break;
1✔
173
                    case "f32.copysign":
174
                        addNumericInstruction( NumericOperator.copysign, ValueType.f32, javaCodePos, lineNumber );
1✔
175
                        break;
1✔
176
                    case "f32.sqrt":
177
                        addNumericInstruction( NumericOperator.sqrt, ValueType.f32, javaCodePos, lineNumber );
1✔
178
                        break;
1✔
179
                    case "f32.sub":
180
                        addNumericInstruction( NumericOperator.sub, ValueType.f32, javaCodePos, lineNumber );
1✔
181
                        break;
1✔
182
                    case "f32.trunc":
183
                        addNumericInstruction( NumericOperator.trunc, ValueType.f32, javaCodePos, lineNumber );
1✔
184
                        break;
1✔
185
                    case "f64.abs":
186
                        addNumericInstruction( NumericOperator.abs, ValueType.f64, javaCodePos, lineNumber );
1✔
187
                        break;
1✔
188
                    case "f64.ceil":
189
                        addNumericInstruction( NumericOperator.ceil, ValueType.f64, javaCodePos, lineNumber );
1✔
190
                        break;
1✔
191
                    case "f64.const":
192
                        addConstInstruction( Double.parseDouble( get( tokens, ++i ) ), ValueType.f64, javaCodePos, lineNumber );
1✔
193
                        break;
1✔
194
                    case "f64.convert_i64_s":
195
                        addConvertInstruction( ValueTypeConvertion.l2d, javaCodePos, lineNumber );
1✔
196
                        break;
1✔
197
                    case "f64.div":
198
                        addNumericInstruction( NumericOperator.div, ValueType.f64, javaCodePos, lineNumber );
1✔
199
                        break;
1✔
200
                    case "f64.floor":
201
                        addNumericInstruction( NumericOperator.floor, ValueType.f64, javaCodePos, lineNumber );
1✔
202
                        break;
1✔
203
                    case "f64.max":
204
                        addNumericInstruction( NumericOperator.max, ValueType.f64, javaCodePos, lineNumber );
1✔
205
                        break;
1✔
206
                    case "f64.min":
207
                        addNumericInstruction( NumericOperator.min, ValueType.f64, javaCodePos, lineNumber );
1✔
208
                        break;
1✔
209
                    case "f64.mul":
210
                        addNumericInstruction( NumericOperator.mul, ValueType.f64, javaCodePos, lineNumber );
1✔
211
                        break;
1✔
212
                    case "f64.nearest":
213
                        addNumericInstruction( NumericOperator.nearest, ValueType.f64, javaCodePos, lineNumber );
1✔
214
                        break;
1✔
215
                    case "f64.reinterpret_i64":
216
                        addConvertInstruction( ValueTypeConvertion.l2d_re, javaCodePos, lineNumber );
1✔
217
                        break;
1✔
218
                    case "f64.copysign":
219
                        addNumericInstruction( NumericOperator.copysign, ValueType.f64, javaCodePos, lineNumber );
1✔
220
                        break;
1✔
221
                    case "f64.sqrt":
222
                        addNumericInstruction( NumericOperator.sqrt, ValueType.f64, javaCodePos, lineNumber );
1✔
223
                        break;
1✔
224
                    case "f64.sub":
225
                        addNumericInstruction( NumericOperator.sub, ValueType.f64, javaCodePos, lineNumber );
1✔
226
                        break;
1✔
227
                    case "f64.trunc":
228
                        addNumericInstruction( NumericOperator.trunc, ValueType.f64, javaCodePos, lineNumber );
1✔
229
                        break;
1✔
230
                    case "ref.is_null":
231
                        addNumericInstruction( NumericOperator.ifnull, ValueType.i32, javaCodePos, lineNumber );
1✔
232
                        break;
1✔
233
                    case "ref.eq":
234
                        addNumericInstruction( NumericOperator.ref_eq, ValueType.i32, javaCodePos, lineNumber );
1✔
235
                        break;
1✔
236
                    case "table.get":
237
                        addTableInstruction( true, getInt( tokens, ++i), javaCodePos, lineNumber );
1✔
238
                        break;
1✔
239
                    case "table.set":
240
                        addTableInstruction( false, getInt( tokens, ++i), javaCodePos, lineNumber );
1✔
241
                        break;
1✔
242
                    case "call":
243
                        try {
244
                            StringBuilder builder = new StringBuilder( get( tokens, ++i ) );
1✔
245
                            String str;
246
                            do {
247
                                str = get( tokens, ++i );
1✔
248
                                builder.append( str );
1✔
249
                            } while ( !")".equals( str ) );
1✔
250
                            builder.append( get( tokens, ++i ) );
1✔
251
                            FunctionName name = new FunctionName( builder.substring( 1 ) );
1✔
252
                            boolean needThisParameter = "<init>".equals( name.methodName ); // TODO should be do lookup to the classLoader
1✔
253
                            addCallInstruction( name, needThisParameter, javaCodePos, lineNumber );
1✔
254
                        } catch( Exception ex ) {
×
255
                            throw WasmException.create( "The syntax for a function name is $package.ClassName.methodName(paramSignature)returnSignature", ex );
×
256
                        }
1✔
257
                        break;
258
                    case "return":
259
                        addBlockInstruction( WasmBlockOperator.RETURN, null, javaCodePos, lineNumber );
1✔
260
                        break;
1✔
261
                    case "if":
262
                        Object data = ValueType.empty;
1✔
263
                        if( "(".equals( get( tokens, i+1 ) ) ) {
1✔
264
                            i++;
1✔
265
                            if( "result".equals( get( tokens, ++i ) ) && ")".equals( get( tokens, ++i + 1) ) ) {
1✔
266
                                data = ValueType.valueOf( get( tokens, i++ ) );
1✔
267
                            } else {
268
                                throw new WasmException( "Unknown WASM token: " + get( tokens, i-1 ), lineNumber );
×
269
                            }
270
                        }
271
                        addBlockInstruction( WasmBlockOperator.IF, data, javaCodePos, lineNumber );
1✔
272
                        break;
1✔
273
                    case "else":
274
                        addBlockInstruction( WasmBlockOperator.ELSE, null, javaCodePos, lineNumber );
1✔
275
                        break;
1✔
276
                    case "end":
277
                        addBlockInstruction( WasmBlockOperator.END, null, javaCodePos, lineNumber );
1✔
278
                        break;
1✔
279
                    case "drop":
280
                        addBlockInstruction( WasmBlockOperator.DROP, null, javaCodePos, lineNumber );
1✔
281
                        break;
1✔
282
                    case "loop":
283
                        addBlockInstruction( WasmBlockOperator.LOOP, null, javaCodePos, lineNumber );
1✔
284
                        break;
1✔
285
                    case "br":
286
                        addBlockInstruction( WasmBlockOperator.BR, getInt( tokens, ++i ), javaCodePos, lineNumber );
1✔
287
                        break;
1✔
288
                    case "br_if":
289
                        addBlockInstruction( WasmBlockOperator.BR_IF, getInt( tokens, ++i ), javaCodePos, lineNumber );
1✔
290
                        break;
1✔
291
                    case "br_on_null":
292
                        addBlockInstruction( WasmBlockOperator.BR_ON_NULL, getInt( tokens, ++i ), javaCodePos, lineNumber );
×
293
                        break;
×
294
                    case "throw":
295
                        addBlockInstruction( WasmBlockOperator.THROW, null, javaCodePos, lineNumber );
1✔
296
                        break;
1✔
297
                    case "unreachable":
298
                        addBlockInstruction( WasmBlockOperator.UNREACHABLE, null, javaCodePos, lineNumber );
1✔
299
                        break;
1✔
300
                    case "i32.load":
301
                        i = addMemoryInstruction( MemoryOperator.load, ValueType.i32, tokens, i, lineNumber );
1✔
302
                        break;
1✔
303
                    case "i32.load8_u":
304
                        i = addMemoryInstruction( MemoryOperator.load8_u, ValueType.i32, tokens, i, lineNumber );
1✔
305
                        break;
1✔
306
                    case "struct.get":
307
                    case "struct.set":
308
                        StructOperator op = "struct.get".equals( tok ) ? StructOperator.GET : StructOperator.SET;
1✔
309
                        String typeName = get( tokens, ++i );
1✔
310
                        String fieldName = get( tokens, ++i );
1✔
311
                        NamedStorageType fieldNameType = null;
1✔
312
                        List<NamedStorageType> fields = getTypeManager().valueOf( typeName ).getFields();
1✔
313
                        if( fields != null ) { // field is null on prepare
1✔
314
                            for( NamedStorageType namedStorageType : fields ) {
1✔
315
                                if( namedStorageType.getName().equals( fieldName ) ) {
1✔
316
                                    fieldNameType = namedStorageType;
1✔
317
                                    break;
1✔
318
                                }
319
                            }
1✔
320
                        }
321
                        if( fieldNameType == null ) {
1✔
322
                            fieldNameType = new NamedStorageType( ValueType.externref, "", fieldName );
1✔
323
                        }
324
                        addStructInstruction( op, typeName, fieldNameType, javaCodePos, lineNumber );
1✔
325
                        break;
1✔
326
                    case "array.len":
327
                        typeName = get( tokens, ++i );
×
328
                        AnyType type = ((ArrayType)getTypeManager().valueOf( typeName )).getArrayType();
×
329
                        addArrayInstruction( ArrayOperator.LEN, type, javaCodePos, lineNumber );
×
330
                        break;
×
331
                    case "array.new_default_with_rtt":
332
                        typeName = get( tokens, ++i );
1✔
333
                        type = ((ArrayType)getTypeManager().valueOf( typeName )).getArrayType();
1✔
334
                        addArrayInstruction( ArrayOperator.NEW_ARRAY_WITH_RTT, type, javaCodePos, lineNumber );
1✔
335
                        break;
1✔
336
                    case "rtt.canon":
337
                        typeName = get( tokens, ++i );
1✔
338
                        addStructInstruction( StructOperator.RTT_CANON, typeName, null, javaCodePos, lineNumber );
1✔
339
                        break;
1✔
340
                    case "struct.new_with_rtt":
341
                        typeName = get( tokens, ++i );
1✔
342
                        addStructInstruction( StructOperator.NEW_WITH_RTT, typeName, null, javaCodePos, lineNumber );
1✔
343
                        break;
1✔
344
                    case "struct.new_default": // Create instance without executing the constructor, works also with nonGC output
345
                        typeName = get( tokens, ++i );
1✔
346
                        addStructInstruction( StructOperator.NEW_DEFAULT, typeName, null, javaCodePos, lineNumber );
1✔
347
                        break;
1✔
348
                    case "array.get":
349
                    case "array.set":
350
                        typeName = get( tokens, ++i );
×
351
                        type = ((ArrayType)getTypeManager().valueOf( typeName )).getArrayType();
×
352
                        addArrayInstruction( "array.get".equals( tok ) ? ArrayOperator.GET : ArrayOperator.SET, type, javaCodePos, lineNumber );
×
353
                        break;
×
354
                    default:
355
                        throw new WasmException( "Unknown WASM token: " + tok, lineNumber );
1✔
356
                }
357
            }
358
        } catch( Throwable ex ) {
1✔
359
            throw WasmException.create( ex, lineNumber );
1✔
360
        }
1✔
361
    }
1✔
362

363
    /**
364
     * Get the token at given position as int.
365
     * 
366
     * @param tokens
367
     *            the token list
368
     * @param idx
369
     *            the position in the tokens
370
     * @return the int value
371
     */
372
    private int getInt( List<String> tokens, @Nonnegative int idx ) {
373
        return Integer.parseInt( get( tokens, idx ) );
1✔
374
    }
375

376
    /**
377
     * Get the token at given position
378
     * 
379
     * @param tokens
380
     *            the token list
381
     * @param idx
382
     *            the position in the tokens
383
     * @return the token
384
     */
385
    @Nonnull
386
    private String get( List<String> tokens, @Nonnegative int idx ) {
387
        if( idx >= tokens.size() ) {
1✔
388
            String previous = tokens.get( Math.min( idx, tokens.size() ) - 1 );
1✔
389
            throw new WasmException( "Missing Token in wasm text format after token: " + previous, -1 );
1✔
390
        }
391
        return tokens.get( idx );
1✔
392
    }
393

394
    /**
395
     * Split the string in tokens.
396
     * 
397
     * @param wat
398
     *            string with wasm text format
399
     * @return the token list.
400
     */
401
    private List<String> splitTokens( @Nullable String wat ) {
402
        ArrayList<String> tokens = new ArrayList<>();
1✔
403
        int count = wat.length();
1✔
404

405
        int off = 0;
1✔
406
        for( int i = 0; i < count; i++ ) {
1✔
407
            char ch = wat.charAt( i );
1✔
408
            switch( ch ) {
1✔
409
                case ' ':
410
                case '\n':
411
                case '\r':
412
                case '\t':
413
                case '(':
414
                case ')':
415
                    if( off < i ) {
1✔
416
                        tokens.add( wat.substring( off, i ) );
1✔
417
                    }
418
                    off = i + 1;
1✔
419
                    switch(ch) {
1✔
420
                        case '(':
421
                            tokens.add( "(" );
1✔
422
                            break;
1✔
423
                        case ')':
424
                            tokens.add( ")" );
1✔
425
                            break;
426
                    }
427
                    break;
428
            }
429
        }
430
        if( off < count ) {
1✔
431
            tokens.add( wat.substring( off, count ) );
1✔
432
        }
433
        return tokens;
1✔
434
    }
435

436
    /**
437
     * Parse the optional tokens of a load memory instruction and add it.
438
     * 
439
     * @param op
440
     *            the operation
441
     * @param type
442
     *            the type of the static field
443
     * @param tokens
444
     *            the token list
445
     * @param i
446
     *            the position in the tokens
447
     * @param lineNumber
448
     *            the line number in the Java source code
449
     * @return the current index to the tokens
450
     */
451
    private int addMemoryInstruction( MemoryOperator op, ValueType type, List<String> tokens, int i, int lineNumber ) {
452
        int offset = 0;
1✔
453
        int alignment = 0;
1✔
454
        if( i < tokens.size() ) {
1✔
455
            String str = tokens.get( i + 1 );
1✔
456
            if( str.startsWith( "offset=" ) ) {
1✔
457
                offset = Integer.parseInt( str.substring( 7 ) );
1✔
458
                i++;
1✔
459
            }
460
            str = tokens.get( i + 1 );
1✔
461
            if( str.startsWith( "align=" ) ) {
1✔
462
                int align = Integer.parseInt( str.substring( 6 ) );
1✔
463
                switch( align ) {
1✔
464
                    case 1:
465
                        alignment = 0;
1✔
466
                        break;
1✔
467
                    case 2:
468
                        alignment = 1;
×
469
                        break;
×
470
                    case 4:
471
                        alignment = 2;
1✔
472
                        break;
1✔
473
                    default:
474
                        throw new WasmException( "alignment must be power-of-two", lineNumber );
×
475
                }
476
                i++;
1✔
477
            }
478
        }
479
        addMemoryInstruction( op, type, offset, alignment, i, lineNumber );
1✔
480
        return i;
1✔
481
    }
482
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc