aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/Sync.java
blob: b7bb4b561e857e94164c048f5742a273a8d1c04a (plain) (blame)
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package invalid.lena.scrcpy;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

// Pure-java adb sync v1 SEND framing. Extracted from Server.push() so
// it can be unit-tested without an AdbStream or any android coupling.
//
// Layout on the wire (little-endian uint32 lengths):
//   "SEND" | u32(len(path,mode)) | path,mode
//   "DATA" | u32(chunk_size)     | chunk_bytes      (repeats, chunk <= 64K)
//   "DONE" | u32(mtime_seconds)
//   ----> response:
//   "OKAY" | u32(0)
//     or
//   "FAIL" | u32(msg_len) | msg
public final class Sync {

    public static final int CHUNK = 64 * 1024;

    // FAIL responses carry a short human-readable message; cap what a
    // corrupt or hostile length field can make us allocate.
    private static final int MAX_FAIL_MSG = 4 * 1024;
    private static final int MAX_PATH_BYTES = 1024;

    private Sync() {}

    // Push `src` to `remotePath` with the given mode and mtime, using the
    // already-open `out` and `in` of an adb sync stream. Returns the
    // total payload byte count.
    public static long push(InputStream src, OutputStream out, InputStream in,
            String remotePath, int mode, int mtimeSec)
            throws IOException {
        Objects.requireNonNull(src);
        Objects.requireNonNull(out);
        Objects.requireNonNull(in);
        Objects.requireNonNull(remotePath);
        if (remotePath.isEmpty() || remotePath.indexOf(',') >= 0
                || remotePath.indexOf('\0') >= 0) {
            throw new IllegalArgumentException("invalid sync path");
        }
        if (mode < 0) throw new IllegalArgumentException("invalid sync mode");
        String header = remotePath + "," + mode;
        byte[] hb = header.getBytes(StandardCharsets.UTF_8);
        if (hb.length > MAX_PATH_BYTES) {
            throw new IllegalArgumentException("sync path and mode are too long");
        }

        byte[] tag = new byte[8];
        putTag(tag, 0, "SEND");
        Wire.writeLe32(tag, 4, hb.length);
        out.write(tag);
        out.write(hb);

        byte[] chunk = new byte[CHUNK];
        byte[] dataHdr = new byte[8];
        putTag(dataHdr, 0, "DATA");
        long total = 0;
        int n;
        while ((n = src.read(chunk)) >= 0) {
            if (n == 0) throw new IOException("sync source made no progress");
            Wire.writeLe32(dataHdr, 4, n);
            out.write(dataHdr);
            out.write(chunk, 0, n);
            total += n;
        }

        byte[] done = new byte[8];
        putTag(done, 0, "DONE");
        Wire.writeLe32(done, 4, mtimeSec);
        out.write(done);
        out.flush();

        byte[] resp = new byte[8];
        Wire.readFully(in, resp);
        String code = new String(resp, 0, 4, StandardCharsets.US_ASCII);
        int len = Wire.readLe32(resp, 4);
        if ("OKAY".equals(code)) {
            if (len != 0) throw new IOException("sync OKAY length is not zero: " + len);
            return total;
        }
        if (!"FAIL".equals(code)) {
            throw new IOException("unknown sync response: " + safeCode(code));
        }

        if (len < 0 || len > MAX_FAIL_MSG) {
            throw new IOException("sync " + code + " response length out of range: " + len);
        }
        byte[] msg = new byte[len];
        if (msg.length > 0) Wire.readFully(in, msg);
        throw new IOException("sync FAIL: " + safeMessage(Wire.decodeUtf8(msg)));
    }

    private static void putTag(byte[] dst, int off, String tag) {
        byte[] b = tag.getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(b, 0, dst, off, 4);
    }

    private static String safeCode(String code) {
        StringBuilder out = new StringBuilder(code.length());
        for (int i = 0; i < code.length(); i++) {
            char c = code.charAt(i);
            out.append(c >= 0x20 && c <= 0x7e ? c : '?');
        }
        return out.toString();
    }

    private static String safeMessage(String message) {
        StringBuilder out = new StringBuilder(message.length());
        for (int i = 0; i < message.length(); i++) {
            char c = message.charAt(i);
            int type = Character.getType(c);
            out.append(Character.isISOControl(c) || type == Character.FORMAT ? '?' : c);
        }
        return out.toString();
    }
}