1
+ package uk .co .playerdata .reactnativemcumanager ;
2
+
3
+ import android .util .Log ;
4
+
5
+ import androidx .annotation .Keep ;
6
+ import androidx .annotation .NonNull ;
7
+
8
+ import com .google .gson .FieldNamingPolicy ;
9
+ import com .google .gson .Gson ;
10
+ import com .google .gson .GsonBuilder ;
11
+
12
+ import java .io .ByteArrayInputStream ;
13
+ import java .io .ByteArrayOutputStream ;
14
+ import java .io .File ;
15
+ import java .io .IOException ;
16
+ import java .io .InputStreamReader ;
17
+ import java .util .HashMap ;
18
+ import java .util .Map ;
19
+ import java .util .zip .ZipEntry ;
20
+ import java .util .zip .ZipInputStream ;
21
+
22
+ import io .runtime .mcumgr .dfu .mcuboot .model .ImageSet ;
23
+ import io .runtime .mcumgr .dfu .mcuboot .model .TargetImage ;
24
+ import io .runtime .mcumgr .exception .McuMgrException ;
25
+
26
+ public final class ZipPackage {
27
+ private static final String MANIFEST = "manifest.json" ;
28
+
29
+ @ SuppressWarnings ({"unused" , "MismatchedReadAndWriteOfArray" })
30
+ @ Keep
31
+ private static class Manifest {
32
+ private int formatVersion ;
33
+ private File [] files ;
34
+
35
+ @ Keep
36
+ private static class File {
37
+ /**
38
+ * The file type. Expected vales are: "application", "bin", "suit-envelope".
39
+ */
40
+ private String type ;
41
+ /**
42
+ * The name of the image file.
43
+ */
44
+ private String file ;
45
+ /**
46
+ * The size of the image file in bytes. This is declared size and does not have to
47
+ * be equal to the actual file size.
48
+ */
49
+ private int size ;
50
+ /**
51
+ * Image index is used for multi-core devices. Index 0 is the main core (app core),
52
+ * index 1 is secondary core (net core), etc.
53
+ * <p>
54
+ * For single-core devices this is not present in the manifest file and defaults to 0.
55
+ */
56
+ private int imageIndex = 0 ;
57
+ /**
58
+ * The slot number where the image is to be sent. By default images are sent to the
59
+ * secondary slot and then swapped to the primary slot after the image is confirmed
60
+ * and the device is reset.
61
+ * <p>
62
+ * However, if the device supports Direct XIP feature it is possible to run an app
63
+ * from a secondary slot. The image has to be compiled for this slot. A ZIP package
64
+ * can contain images for both slots. Only the one targeting the available one will
65
+ * be sent.
66
+ * @since NCS v 2.5, nRF Connect Device Manager 1.8.
67
+ */
68
+ private int slot = TargetImage .SLOT_SECONDARY ;
69
+ }
70
+ }
71
+
72
+ private Manifest manifest ;
73
+ private final Map <String , byte []> entries = new HashMap <>();
74
+
75
+ public ZipPackage (@ NonNull final byte [] data ) throws IOException {
76
+ ZipEntry ze ;
77
+
78
+ // Unzip the file and look for the manifest.json.
79
+ final ZipInputStream zis = new ZipInputStream (new ByteArrayInputStream (data ));
80
+ while ((ze = zis .getNextEntry ()) != null ) {
81
+ if (ze .isDirectory ())
82
+ throw new IOException ("Invalid ZIP" );
83
+
84
+ final String name = validateFilename (ze .getName (), "." );
85
+
86
+ if (name .equals (MANIFEST )) {
87
+ final Gson gson = new GsonBuilder ()
88
+ .setFieldNamingPolicy (FieldNamingPolicy .LOWER_CASE_WITH_UNDERSCORES )
89
+ .create ();
90
+ manifest = gson .fromJson (new InputStreamReader (zis ), Manifest .class );
91
+ } else if (name .endsWith (".bin" ) || name .endsWith (".suit" )) {
92
+ final byte [] content = getData (zis );
93
+ entries .put (name , content );
94
+ } else {
95
+ throw new IOException ("Unsupported file found: " + name );
96
+ }
97
+ }
98
+ }
99
+
100
+ public ImageSet getBinaries () throws IOException , McuMgrException {
101
+ final ImageSet binaries = new ImageSet ();
102
+
103
+ // Search for images.
104
+ for (final Manifest .File file : manifest .files ) {
105
+ final String name = file .file ;
106
+ final byte [] content = entries .get (name );
107
+ if (content == null )
108
+ throw new IOException ("File not found: " + name );
109
+
110
+ binaries .add (new TargetImage (file .imageIndex , file .slot , content ));
111
+ }
112
+ return binaries ;
113
+ }
114
+
115
+ public byte [] getSuitEnvelope () {
116
+ // First, search for an entry of type "suit-envelope".
117
+ for (final Manifest .File file : manifest .files ) {
118
+ if (file .type .equals ("suit-envelope" )) {
119
+ return entries .get (file .file );
120
+ }
121
+ }
122
+ // If not found, search for a file with the ".suit" extension.
123
+ for (final Manifest .File file : manifest .files ) {
124
+ if (file .file .endsWith (".suit" )) {
125
+ return entries .get (file .file );
126
+ }
127
+ }
128
+ // Not found.
129
+ return null ;
130
+ }
131
+
132
+ public byte [] getResource (@ NonNull final String name ) {
133
+ return entries .get (name );
134
+ }
135
+
136
+ private byte [] getData (@ NonNull ZipInputStream zis ) throws IOException {
137
+ final byte [] buffer = new byte [1024 ];
138
+
139
+ // Read file content to byte array
140
+ final ByteArrayOutputStream os = new ByteArrayOutputStream ();
141
+ int count ;
142
+ while ((count = zis .read (buffer )) != -1 ) {
143
+ os .write (buffer , 0 , count );
144
+ }
145
+ return os .toByteArray ();
146
+ }
147
+
148
+ /**
149
+ * Validates the path (not the content) of the zip file to prevent path traversal issues.
150
+ *
151
+ * <p> When unzipping an archive, always validate the compressed files' paths and reject any path
152
+ * that has a path traversal (such as ../..). Simply looking for .. characters in the compressed
153
+ * file's path may not be enough to prevent path traversal issues. The code validates the name of
154
+ * the entry before extracting the entry. If the name is invalid, the entire extraction is aborted.
155
+ * <p>
156
+ *
157
+ * @param filename The path to the file.
158
+ * @param intendedDir The intended directory where the zip should be.
159
+ * @return The validated path to the file.
160
+ * @throws java.io.IOException Thrown in case of path traversal issues.
161
+ */
162
+ @ SuppressWarnings ("SameParameterValue" )
163
+ private String validateFilename (@ NonNull final String filename ,
164
+ @ NonNull final String intendedDir )
165
+ throws IOException {
166
+ File f = new File (filename );
167
+ String canonicalPath = f .getCanonicalPath ();
168
+
169
+ File iD = new File (intendedDir );
170
+ String canonicalID = iD .getCanonicalPath ();
171
+
172
+ if (canonicalPath .startsWith (canonicalID )) {
173
+ return canonicalPath .substring (1 ); // remove leading "/"
174
+ } else {
175
+ throw new IllegalStateException ("File is outside extraction target directory." );
176
+ }
177
+ }
178
+
179
+ }
0 commit comments