1 | // Copyright (C) 2009 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.ancillary.servlet; |
16 | |
17 | import com.google.caja.ancillary.jsdoc.FileSystem; |
18 | import com.google.caja.lexer.CharProducer; |
19 | import com.google.caja.lexer.InputSource; |
20 | import com.google.caja.util.Maps; |
21 | |
22 | import java.io.ByteArrayOutputStream; |
23 | import java.io.FileNotFoundException; |
24 | import java.io.IOException; |
25 | import java.io.OutputStream; |
26 | import java.io.StringWriter; |
27 | import java.io.Writer; |
28 | import java.net.URI; |
29 | import java.net.URISyntaxException; |
30 | import java.util.Map; |
31 | import java.util.regex.Pattern; |
32 | import java.util.zip.ZipEntry; |
33 | import java.util.zip.ZipOutputStream; |
34 | |
35 | /** |
36 | * A virtual file system backed by an in-memory ZIP file. |
37 | * |
38 | * @author mikesamuel@gmail.com |
39 | */ |
40 | class ZipFileSystem implements FileSystem { |
41 | private final String root; |
42 | private final URI rootUri; |
43 | private final Map<String, Content> files = Maps.newLinkedHashMap(); |
44 | |
45 | private static final Pattern BAD_PATH = Pattern.compile( |
46 | "(?:^|/)(?:\\.\\.?)(?:/|$)"); |
47 | private static final Content DIRECTORY = null; |
48 | |
49 | /** |
50 | * @param root an absolute directory under which all files must be organized. |
51 | * E.g. {@code /foo}. |
52 | */ |
53 | ZipFileSystem(String root) { |
54 | if (!root.startsWith("/") || root.endsWith("/")) { |
55 | throw new IllegalArgumentException(root); |
56 | } |
57 | this.root = root; |
58 | this.rootUri = fileUriWithPath(root + "/"); |
59 | files.put("/", DIRECTORY); |
60 | files.put(root, DIRECTORY); |
61 | } |
62 | private static URI fileUriWithPath(String path) { |
63 | try { |
64 | return new URI("file", null, path, null, null); |
65 | } catch (URISyntaxException ex) { |
66 | throw new RuntimeException(ex); |
67 | } |
68 | } |
69 | |
70 | public String basename(String path) { |
71 | int end = path.length(); |
72 | while (end > 0 && path.charAt(end - 1) == '/') { --end; } |
73 | if (end == 0 && path.length() != 0) { return "/"; } |
74 | int lastSlash = path.lastIndexOf('/', end - 1); |
75 | return path.substring(lastSlash + 1, end); |
76 | } |
77 | |
78 | public String canonicalPath(final String path) throws IOException { |
79 | URI canon = rootUri.resolve(path); |
80 | if (canon == null) { throw new IOException(path); } |
81 | String canonPath = canon.getPath(); |
82 | if (BAD_PATH.matcher(canonPath).find()) { throw new IOException(path); } |
83 | canonPath = canonPath.replaceAll("/{2,}", "/"); |
84 | if (canonPath.endsWith("/") && !"/".equals(canonPath)) { |
85 | canonPath = canonPath.substring(0, canonPath.length() - 1); |
86 | } |
87 | if (root.equals(canonPath) || canonPath.startsWith(root + "/")) { |
88 | return canonPath; |
89 | } else { |
90 | throw new IOException(path); |
91 | } |
92 | } |
93 | |
94 | public String dirname(String path) { |
95 | int end = path.length(); |
96 | while (end > 0 && path.charAt(end - 1) == '/') { --end; } |
97 | if (end == 0 && path.length() != 0) { return "/"; } |
98 | |
99 | int lastSlash = path.lastIndexOf('/', end - 1); |
100 | if (lastSlash < 0) { return null; } |
101 | if (lastSlash == 0) { return "/"; } |
102 | return path.substring(0, lastSlash); |
103 | } |
104 | |
105 | public boolean exists(String path) { |
106 | try { path = canonicalPath(path); } catch (IOException ex) { return false; } |
107 | return files.containsKey(path); |
108 | } |
109 | |
110 | public boolean isDirectory(String path) { |
111 | try { path = canonicalPath(path); } catch (IOException ex) { return false; } |
112 | return files.containsKey(path) && DIRECTORY == files.get(path); |
113 | } |
114 | |
115 | public boolean isFile(String path) { |
116 | try { path = canonicalPath(path); } catch (IOException ex) { return false; } |
117 | return files.get(path) != DIRECTORY; |
118 | } |
119 | |
120 | public String join(String dir, String path) { |
121 | if (path == null) { throw new NullPointerException(); } |
122 | if (path.startsWith("/")) { throw new IllegalArgumentException(path); } |
123 | if (dir == null || "".equals(dir)) { return path; } |
124 | if ("".equals(path)) { return dir; } |
125 | return (dir + "/" + path).replace("//", "/"); |
126 | } |
127 | |
128 | private void requireParent(String path) throws IOException { |
129 | String parent = dirname(path); |
130 | if (!(files.containsKey(parent) && files.get(parent) == DIRECTORY)) { |
131 | throw new IOException(parent + " is not a directory"); |
132 | } |
133 | } |
134 | |
135 | public void mkdir(String path) throws IOException { |
136 | path = canonicalPath(path); |
137 | requireParent(path); |
138 | if (files.get(path) != DIRECTORY) { |
139 | throw new IOException(path + " is a file"); |
140 | } |
141 | files.put(path, null); |
142 | } |
143 | |
144 | public CharProducer read(String path) throws IOException { |
145 | path = canonicalPath(path); |
146 | Content content = files.get(path); |
147 | if (content != DIRECTORY) { |
148 | return CharProducer.Factory.fromString( |
149 | content.getText(), toInputSource(path)); |
150 | } else { |
151 | throw new FileNotFoundException(path); |
152 | } |
153 | } |
154 | |
155 | public InputSource toInputSource(String path) { |
156 | return new InputSource(fileUriWithPath(path)); |
157 | } |
158 | |
159 | public Writer write(String path) throws IOException { |
160 | path = canonicalPath(path); |
161 | requireParent(path); |
162 | if (files.containsKey(path) && files.get(path) == DIRECTORY) { |
163 | throw new IOException(path + " is a directory"); |
164 | } |
165 | files.put(path, new Content("", null)); |
166 | final String outPath = path; |
167 | return new StringWriter() { |
168 | @Override |
169 | public void close() throws IOException { |
170 | super.close(); |
171 | files.put(outPath, new Content(this.toString(), null)); |
172 | } |
173 | }; |
174 | } |
175 | |
176 | public OutputStream writeBytes(String path) throws IOException { |
177 | path = canonicalPath(path); |
178 | requireParent(path); |
179 | if (files.containsKey(path) && files.get(path) == DIRECTORY) { |
180 | throw new IOException(path + " is a directory"); |
181 | } |
182 | files.put(path, new Content("", null)); |
183 | final String outPath = path; |
184 | return new ByteArrayOutputStream() { |
185 | @Override |
186 | public void close() throws IOException { |
187 | super.close(); |
188 | files.put(outPath, new Content(this.toByteArray(), null)); |
189 | } |
190 | }; |
191 | } |
192 | |
193 | public Job toZip() throws IOException { |
194 | ByteArrayOutputStream zippedBytes = new ByteArrayOutputStream(); |
195 | ZipOutputStream zipOut = new ZipOutputStream(zippedBytes); |
196 | for (Map.Entry<String, Content> file : files.entrySet()) { |
197 | String path = file.getKey(); |
198 | Content content = file.getValue(); |
199 | if (content == DIRECTORY) { |
200 | if ("/".equals(path)) { continue; } |
201 | // Zip file format treats paths that end in "/" as dirs. |
202 | ZipEntry ze = new ZipEntry(path + "/"); |
203 | zipOut.putNextEntry(ze); |
204 | } else { |
205 | ZipEntry ze = new ZipEntry(path); |
206 | ze.setSize(content.byteLength()); |
207 | zipOut.putNextEntry(ze); |
208 | content.toOutputStream(zipOut); |
209 | } |
210 | } |
211 | zipOut.close(); |
212 | return Job.zip(zippedBytes.toByteArray()); |
213 | } |
214 | } |