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

ehrmann / vcdiff-java / 26

18 Apr 2026 05:11PM UTC coverage: 83.839% (+0.2%) from 83.679%
26

push

circleci

ehrmann
Fix Coveralls integration

663 of 814 branches covered (81.45%)

1603 of 1912 relevant lines covered (83.84%)

0.84 hits per line

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

78.71
/cli/src/main/java/com/davidehrmann/vcdiff/VCDiffFileBasedCoder.java
1
// Copyright 2008-2016 Google Inc., David Ehrmann
2
// Author: Lincoln Smith, David Ehrmann
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
package com.davidehrmann.vcdiff;
17

18
import com.beust.jcommander.IParameterValidator;
19
import com.beust.jcommander.JCommander;
20
import com.beust.jcommander.Parameter;
21
import com.beust.jcommander.ParameterException;
22
import com.beust.jcommander.Parameters;
23
import com.beust.jcommander.ParametersDelegate;
24
import com.davidehrmann.vcdiff.io.ComparingOutputStream;
25
import com.davidehrmann.vcdiff.io.CountingInputStream;
26
import com.davidehrmann.vcdiff.io.CountingOutputStream;
27
import com.davidehrmann.vcdiff.io.IOUtils;
28

29
import java.io.FileInputStream;
30
import java.io.FileNotFoundException;
31
import java.io.FileOutputStream;
32
import java.io.FilterInputStream;
33
import java.io.FilterOutputStream;
34
import java.io.IOException;
35
import java.io.InputStream;
36
import java.io.OutputStream;
37
import java.util.Arrays;
38
import java.util.LinkedList;
39
import java.util.List;
40
import java.util.ListIterator;
41

42
/**
43
 * / command-line interface to the open-vcdiff library.
44
 */
45
public class VCDiffFileBasedCoder {
46
    public static final int DEFAULT_MAX_TARGET_SIZE = 1 << 26;      // 64 MB
47

48
    public static class PositiveInteger implements IParameterValidator {
1✔
49
        public void validate(String name, String value)  throws ParameterException {
50
            int n = Integer.parseInt(value);
1✔
51
            if (n <= 0) {
1✔
52
                throw new ParameterException("Parameter " + name + " should be positive (found " + value +")");
1✔
53
            }
54
        }
1✔
55
    }
56

57
    // Definitions of command-line flags
58
    private static class OptionalTargetAndDeltaOptions {
59
        @Parameter(names = {"-target", "--target"}, description = "Target file (default is stdin for encode, stdout for decode)")
60
        protected String target;
61

62
        @Parameter(names = {"-delta", "--delta"}, description = "Encoded delta file (default is stdout for encode, stdin for decode)")
63
        protected String delta;
64
    }
65

66
    private static class RequiredTargetAndDeltaOptions {
67
        @Parameter(names = {"-target", "--target"}, description = "Target file", required = true)
68
        protected String target;
69

70
        @Parameter(names = {"-delta", "--delta"}, description = "Encoded delta file", required = true)
71
        protected String delta;
72
    }
73

74
    protected static class EncodeOptions {
1✔
75
        @Parameter(names = {"-checksum", "--checksum"}, description = "Include an Adler32 checksum of the target data when encoding")
1✔
76
        protected boolean checksum = false;
77

78
        @Parameter(names = {"-interleaved", "--interleaved"}, description = "Use interleaved format")
1✔
79
        protected boolean interleaved = false;
80

81
        @Parameter(names = {"-target_matches", "--target_matches"}, description = "Find duplicate strings in target data as well as dictionary data")
1✔
82
        protected boolean targetMatches = false;
83
    }
84

85
    protected static class DecodeOptions {
1✔
86
        @Parameter(names = {"-allow_vcd_target", "--allow_vcd_target"}, description = "If false, the decoder issues an error when the VCD_TARGET flag is encountered")
1✔
87
        protected boolean allowVcdTarget = true;
88
    }
89

90
    protected static class GlobalOptions {
1✔
91
        @Parameter(names = {"-dictionary", "--dictionary"}, description = "File containing dictionary data (required)", required = true)
92
        protected String dictionary;
93

94
        @Parameter(names = {"-max_target_file_size", "--max_target_file_size"}, description = "Maximum target file size allowed by decoder")
1✔
95
        protected long maxTargetFileSize = (long) DEFAULT_MAX_TARGET_SIZE;
96

97
        @Parameter(names = {"-max_target_window_size", "--max_target_window_size"}, description = "Maximum target window size allowed by decoder")
1✔
98
        protected int maxTargetWindowSize = DEFAULT_MAX_TARGET_SIZE;
99

100
        // --buffersize is the maximum allowable size of a target window.
101
        // This value may be increased if there is sufficient memory available.
102
        @Parameter(names = {"-buffersize", "--buffersize"}, description = "Buffer size for reading input file", validateWith = PositiveInteger.class)
1✔
103
        protected int bufferSize = 1 << 20;
104

105
        @Parameter(names = {"-stats", "--stats"}, description = "Report compression percentage")
1✔
106
        protected boolean stats = false;
107
    }
108

109
    private VCDiffFileBasedCoder() {
110

111
    }
112

113
    // Opens a file for incremental reading.  file_name is the name of the file
114
    // to be opened.  file_type should be a descriptive name (like "target") for
115
    // use in log messages.
116
    private static InputStream OpenFileForReading(String file_name, String file_type) throws FileNotFoundException{
117
        try {
118
            return new InputStreamExceptionMapper(
1✔
119
                    new FileInputStream(file_name),
120
                    file_type
121
            );
122
        } catch (FileNotFoundException e) {
1✔
123
            throw new FileNotFoundException(String.format(
1✔
124
                    "Error opening %s file: %s",
125
                    file_type, e.getMessage()
1✔
126
            ));
127
        }
128
    }
129

130
    private static OutputStream OpenFileForWriting(String file_name, String file_type) throws FileNotFoundException{
131
        try {
132
            return new OutputStreamExceptionMapper(
1✔
133
                    new FileOutputStream(file_name),
134
                    file_type
135
            );
136
        } catch (FileNotFoundException e) {
×
137
            throw new FileNotFoundException(String.format(
×
138
                    "Error opening %s file: %s",
139
                    file_type, e.getMessage()
×
140
            ));
141
        }
142
    }
143

144
    // Opens the dictionary file and reads it into a newly allocated buffer.
145
    // If successful, returns true and populates dictionary with the dictionary
146
    // contents; otherwise, returns the buffer.
147
    protected static byte[] OpenDictionary(String dictionary) throws IOException {
148
        try (InputStream in = OpenFileForReading(dictionary, "dictionary")) {
1✔
149
            return IOUtils.toByteArray(in);
1✔
150
        }
151
    }
152

153
    protected static class InputStreamExceptionMapper extends FilterInputStream {
154

155
        private final String type;
156

157
        InputStreamExceptionMapper(InputStream in, String type) {
158
            super(in);
1✔
159
            this.type = type;
1✔
160
        }
1✔
161

162
        @Override
163
        public int read() throws IOException {
164
            try {
165
                return super.read();
×
166
            } catch (IOException e) {
×
167
                throw new IOException(String.format(
×
168
                        "Error reading from %s file: %s%n",
169
                        type,
170
                        e.getMessage()
×
171
                ));
172
            }
173
        }
174

175
        @Override
176
        public int read(byte[] b, int off, int len) throws IOException {
177
            try {
178
                return super.read(b, off, len);
1✔
179
            } catch (IOException e) {
×
180
                throw new IOException(String.format(
×
181
                        "Error reading from %s file: %s%n",
182
                        type,
183
                        e.getMessage()
×
184
                ));
185
            }
186
        }
187

188
        @Override
189
        public long skip(long n) throws IOException {
190
            try {
191
                return super.skip(n);
×
192
            } catch (IOException e) {
×
193
                throw new IOException(String.format(
×
194
                        "Error reading from %s file: %s%n",
195
                        type,
196
                        e.getMessage()
×
197
                ));
198
            }
199
        }
200

201
        @Override
202
        public int available() throws IOException {
203
            try {
204
                return super.available();
×
205
            } catch (IOException e) {
×
206
                throw new IOException(String.format(
×
207
                        "Error reading from %s file: %s%n",
208
                        type,
209
                        e.getMessage()
×
210
                ));
211
            }
212
        }
213

214
        @Override
215
        public void close() throws IOException {
216
            try {
217
                super.close();
1✔
218
            } catch (IOException e) {
×
219
                throw new IOException(String.format(
×
220
                        "Error closing %s file: %s",
221
                        type,
222
                        e.getMessage()
×
223
                ));
224
            }
1✔
225
        }
1✔
226
    }
227

228
    protected static class OutputStreamExceptionMapper extends FilterOutputStream {
229
        private final String type;
230

231
        OutputStreamExceptionMapper(OutputStream out, String type) {
232
            super(out);
1✔
233
            this.type = type;
1✔
234
        }
1✔
235

236
        @Override
237
        public void write(int b) throws IOException {
238
            try {
239
                super.write(b);
1✔
240
            } catch (IOException e) {
×
241
                throw new IOException(String.format(
×
242
                        "Error writing %d byte to %s file: %s",
243
                        1, type, e.getMessage()
×
244
                ));
245
            }
1✔
246
        }
1✔
247

248
        @Override
249
        public void write(byte[] b, int off, int len) throws IOException {
250
            try {
251
                super.out.write(b, off, len);
1✔
252
            } catch (IOException e) {
×
253
                throw new IOException(String.format(
×
254
                        "Error writing %d byte(s) to %s file: %s",
255
                        len, type, e.getMessage()
×
256
                ));
257
            }
1✔
258
        }
1✔
259

260
        @Override
261
        public void flush() throws IOException {
262
            try {
263
                super.flush();
1✔
264
            } catch (IOException e) {
×
265
                throw new IOException(String.format(
×
266
                        "Error flushing %s file: %s",
267
                        type, e.getMessage()
×
268
                ));
269
            }
1✔
270
        }
1✔
271

272
        @Override
273
        public void close() throws IOException {
274
            try {
275
                super.close();
1✔
276
            } catch (IOException e) {
×
277
                throw new IOException(String.format(
×
278
                        "Error closing %s file: %s%n",
279
                        type, e.getMessage()
×
280
                ));
281
            }
1✔
282
        }
1✔
283
    }
284

285
    // Once the command-line arguments have been parsed, these functions
286
    // will use the supplied options to carry out a file-based encode
287
    // or decode operation.
288

289
    @Parameters(commandDescription = "Create delta file from dictionary and target file", separators = " =")
290
    private static class EncodeCommand extends VCDiffFileBasedCoder {
1✔
291

292
        @ParametersDelegate
1✔
293
        private EncodeOptions encodeOptions = new EncodeOptions();
294

295
        @ParametersDelegate
1✔
296
        private GlobalOptions globalOptions = new GlobalOptions();
297

298
        @ParametersDelegate
1✔
299
        private OptionalTargetAndDeltaOptions targetAndDeltaOptions = new OptionalTargetAndDeltaOptions();
300

301
        public void Encode() throws IOException {
302
            byte[] dictionary = OpenDictionary(globalOptions.dictionary);
1✔
303

304
            boolean useStdin = (targetAndDeltaOptions.target == null || targetAndDeltaOptions.target.isEmpty());
1!
305
            boolean useStdout = (targetAndDeltaOptions.delta == null || targetAndDeltaOptions.delta.isEmpty());
1!
306

307
            InputStream fileIn = useStdin ? new InputStreamExceptionMapper(System.in, "target") : OpenFileForReading(targetAndDeltaOptions.target, "target");
1✔
308
            try (CountingInputStream countingIn = new CountingInputStream(fileIn)) {
1✔
309
                OutputStream fileOut = useStdout ? new OutputStreamExceptionMapper(System.out, "delta") : OpenFileForWriting(targetAndDeltaOptions.delta, "delta");
1✔
310
                CountingOutputStream countingOut = new CountingOutputStream(fileOut);
1✔
311
                try (OutputStream vcDiffOut = VCDiffEncoderBuilder.builder()
1✔
312
                        .withDictionary(dictionary)
1✔
313
                        .withTargetMatches(encodeOptions.targetMatches)
1✔
314
                        .withChecksum(encodeOptions.checksum)
1✔
315
                        .withInterleaving(encodeOptions.interleaved)
1✔
316
                        .buildOutputStream(countingOut)) {
1✔
317
                    IOUtils.copyLarge(countingIn, vcDiffOut, new byte[globalOptions.bufferSize]);
1✔
318
                }
319

320
                if (globalOptions.stats && (countingIn.getBytesRead() > 0)) {
1!
321
                    System.err.printf("Original size: %d\tCompressed size: %d (%.2f%% of original)%n",
1✔
322
                            countingIn.getBytesRead(),
1✔
323
                            countingOut.getBytesWritten(),
1✔
324
                            100.0 * countingOut.getBytesWritten() / countingIn.getBytesRead()
1✔
325
                    );
326
                }
327
            }
328
        }
1✔
329
    }
330

331
    @Parameters(commandDescription = "Reconstruct target file from dictionary and delta file", separators = " =")
332
    private static class DecodeCommand extends VCDiffFileBasedCoder {
1✔
333

334
        @ParametersDelegate
1✔
335
        private GlobalOptions globalOptions = new GlobalOptions();
336

337
        @ParametersDelegate
1✔
338
        private DecodeOptions decodeOptions = new DecodeOptions();
339

340
        @ParametersDelegate
1✔
341
        private OptionalTargetAndDeltaOptions targetAndDeltaFlags = new OptionalTargetAndDeltaOptions();
342

343
        void Decode() throws IOException {
344
            byte[] dictionary = OpenDictionary(globalOptions.dictionary);
1✔
345

346
            boolean useStdin = (targetAndDeltaFlags.delta == null || targetAndDeltaFlags.delta.isEmpty());
1!
347
            boolean useStdout = (targetAndDeltaFlags.target == null || targetAndDeltaFlags.target.isEmpty());
1!
348

349
            CountingInputStream countedIn = new CountingInputStream(useStdin ? new InputStreamExceptionMapper(System.in, "delta") : OpenFileForReading(targetAndDeltaFlags.delta, "delta"));
1✔
350
            try (InputStream vcDiffIn = VCDiffDecoderBuilder.builder()
1✔
351
                    .withMaxTargetFileSize(globalOptions.maxTargetFileSize)
1✔
352
                    .withMaxTargetWindowSize(globalOptions.maxTargetWindowSize)
1✔
353
                    .withAllowTargetMatches(decodeOptions.allowVcdTarget)
1✔
354
                    .buildInputStream(countedIn, dictionary);
1✔
355
                 CountingOutputStream out = new CountingOutputStream(useStdout ?
1✔
356
                         new OutputStreamExceptionMapper(System.out, "target") :
1✔
357
                         OpenFileForWriting(targetAndDeltaFlags.target, "target"))) {
1✔
358
                IOUtils.copyLarge(vcDiffIn, out, new byte[globalOptions.bufferSize]);
1✔
359

360
                if (globalOptions.stats && (out.getBytesWritten() > 0)) {
1!
361
                    System.err.printf("Decompressed size: %d\tCompressed size: %d (%.2f%% of original)%n",
1✔
362
                            out.getBytesWritten(),
1✔
363
                            countedIn.getBytesRead(),
1✔
364
                            100.0 * countedIn.getBytesRead() / out.getBytesWritten()
1✔
365
                    );
366
                }
367
            }
368
        }
1✔
369
    }
370

371
    // for "vcdiff test"; compare target with original
372
    @Parameters(hidden = true, separators = " =")
373
    private static class DecodeAndCompareCommand extends VCDiffFileBasedCoder {
1✔
374

375
        @ParametersDelegate
1✔
376
        private GlobalOptions globalOptions = new GlobalOptions();
377

378
        @ParametersDelegate
1✔
379
        private EncodeOptions encodeOptions = new EncodeOptions();
380

381
        @ParametersDelegate
1✔
382
        private DecodeOptions decodeOptions = new DecodeOptions();
383

384
        @ParametersDelegate
1✔
385
        private RequiredTargetAndDeltaOptions targetAndDeltaOptions = new RequiredTargetAndDeltaOptions();
386

387
        void DecodeAndCompare() throws IOException {
388
            byte[] dictionary = OpenDictionary(globalOptions.dictionary);
1✔
389

390
            try (CountingInputStream countedIn = new CountingInputStream(OpenFileForReading(targetAndDeltaOptions.delta, "delta"));
1✔
391
                 InputStream in = VCDiffDecoderBuilder.builder()
1✔
392
                         .withMaxTargetFileSize(globalOptions.maxTargetFileSize)
1✔
393
                         .withMaxTargetWindowSize(globalOptions.maxTargetWindowSize)
1✔
394
                         .withAllowTargetMatches(decodeOptions.allowVcdTarget)
1✔
395
                         .buildInputStream(countedIn, dictionary);
1✔
396
                 InputStream expected = OpenFileForReading(targetAndDeltaOptions.target, "target");
1✔
397
                 CountingOutputStream out = new CountingOutputStream(new ComparingOutputStream(expected))) {
1✔
398
                IOUtils.copyLarge(in, out, new byte[globalOptions.bufferSize]);
1✔
399

400
                // Close out here so it verifies EOF
401
                out.close();
1✔
402

403
                if (globalOptions.stats && (out.getBytesWritten() > 0)) {
1!
404
                    System.err.printf("Decompressed size: %d\tCompressed size: %d (%.2f%% of original)%n",
1✔
405
                            out.getBytesWritten(),
1✔
406
                            countedIn.getBytesRead(),
1✔
407
                            100.0 * countedIn.getBytesRead() / out.getBytesWritten()
1✔
408
                    );
409
                }
410
            }
411
        }
1✔
412
    }
413

414
    public static void main(String[] argv) throws Exception {
415
        System.exit(run(argv));
×
416
    }
×
417

418
    static int run(String[] argv) throws Exception {
419

420
        // TODO: JCommander has an issue with boolean arity. Rewrite allow_vcd_target.
421
        List<String> newArgv = new LinkedList<String>(Arrays.asList(argv));
1✔
422

423
        ListIterator<String> i = newArgv.listIterator();
1✔
424
        while (i.hasNext()) {
1✔
425
            String arg = i.next();
1✔
426
            if (arg.matches("--?allow_vcd_target") && i.hasNext()) {
1!
427
                String val = i.next();
×
428
                if (val.matches("(?i)true|yes|1")) {
×
429
                    i.remove();
×
430
                } else if (val.matches("(?i)false|no|0")) {
×
431
                    i.remove();
×
432
                    i.previous();
×
433
                    i.remove();
×
434
                } else {
435
                    i.previous();
×
436
                }
437
            } else if (arg.matches("--?allow_vcd_target(=.*)?")) {
1✔
438
                if (arg.matches("(?i)--?allow_vcd_target=(true|yes|1)")) {
1✔
439
                    i.set("-allow_vcd_target");
1✔
440
                } else if (arg.matches("(?i)--?allow_vcd_target=(false|no|0)")) {
1!
441
                    i.remove();
1✔
442
                }
443
            }
444

445
            argv = newArgv.toArray(new String[newArgv.size()]);
1✔
446
        }
1✔
447

448
        EncodeCommand encodeCommand = new EncodeCommand();
1✔
449
        DecodeCommand decodeCommand = new DecodeCommand();
1✔
450
        DecodeAndCompareCommand decodeAndCompareCommand = new DecodeAndCompareCommand();
1✔
451

452
        JCommander jCommander = new JCommander();
1✔
453
        jCommander.addCommand("encode", encodeCommand, "delta");
1✔
454
        jCommander.addCommand("decode", decodeCommand, "patch");
1✔
455
        jCommander.addCommand("test", decodeAndCompareCommand);
1✔
456

457
        try {
458
            jCommander.parse(argv);
1✔
459
        } catch (ParameterException e) {
1✔
460
            System.err.println(e.getMessage());
1✔
461
            return 1;
1✔
462
        }
1✔
463

464
        String command_name = VCDiffFileBasedCoder.class.getCanonicalName();
1✔
465
        String command_option = jCommander.getParsedCommand();
1✔
466

467
        try {
468
            if ("encode".equals(command_option) || "delta".equals(command_option)) {
1!
469
                encodeCommand.Encode();
1✔
470
            } else if ("decode".equals(command_option) || "patch".equals(command_option)) {
1!
471
                decodeCommand.Decode();
1✔
472
            } else if ("test".equals(command_option)) {
1✔
473
                // "vcdiff test" does not appear in the usage string, but can be
474
                // used for debugging.  It encodes, then decodes, then compares the result
475
                // with the original target. It expects the same arguments as
476
                // "vcdiff encode", with the additional requirement that the --target
477
                // and --delta file arguments must be specified, rather than using stdin
478
                // or stdout.  It produces a delta file just as for "vcdiff encode".
479

480
                // TODO: the test command is kludgy
481
                JCommander jCommander2 = new JCommander();
1✔
482
                jCommander2.addObject(encodeCommand);
1✔
483
                jCommander2.parse(Arrays.copyOfRange(argv, 1, argv.length));
1✔
484

485
                encodeCommand.Encode();
1✔
486

487
                decodeAndCompareCommand.DecodeAndCompare();
1✔
488
            } else {
1✔
489
                System.err.printf("%s: Unrecognized command option %s%n", command_name, command_option);
1✔
490
                jCommander.usage();
1✔
491
                return 1;
1✔
492
            }
493
        } catch (IOException e) {
1✔
494
            System.err.println(e.getMessage());
1✔
495
            return 1;
1✔
496
        }
1✔
497
        return 0;
1✔
498
    }
499
}
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

© 2026 Coveralls, Inc