1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.commons.compress.archivers.ar;
20
21 import static java.nio.charset.StandardCharsets.US_ASCII;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.OutputStream;
26 import java.nio.file.LinkOption;
27 import java.nio.file.Path;
28
29 import org.apache.commons.compress.archivers.ArchiveOutputStream;
30 import org.apache.commons.compress.utils.ArchiveUtils;
31
32
33
34
35
36
37 public class ArArchiveOutputStream extends ArchiveOutputStream<ArArchiveEntry> {
38
39 public static final int LONGFILE_ERROR = 0;
40
41
42 public static final int LONGFILE_BSD = 1;
43
44 private final OutputStream out;
45 private long entryOffset;
46 private ArArchiveEntry prevEntry;
47 private boolean haveUnclosedEntry;
48 private int longFileMode = LONGFILE_ERROR;
49
50
51 private boolean finished;
52
53 public ArArchiveOutputStream(final OutputStream out) {
54 this.out = out;
55 }
56
57
58
59
60 @Override
61 public void close() throws IOException {
62 try {
63 if (!finished) {
64 finish();
65 }
66 } finally {
67 out.close();
68 prevEntry = null;
69 }
70 }
71
72 @Override
73 public void closeArchiveEntry() throws IOException {
74 if (finished) {
75 throw new IOException("Stream has already been finished");
76 }
77 if (prevEntry == null || !haveUnclosedEntry) {
78 throw new IOException("No current entry to close");
79 }
80 if (entryOffset % 2 != 0) {
81 out.write('\n');
82 }
83 haveUnclosedEntry = false;
84 }
85
86 @Override
87 public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
88 if (finished) {
89 throw new IOException("Stream has already been finished");
90 }
91 return new ArArchiveEntry(inputFile, entryName);
92 }
93
94
95
96
97
98
99 @Override
100 public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
101 if (finished) {
102 throw new IOException("Stream has already been finished");
103 }
104 return new ArArchiveEntry(inputPath, entryName, options);
105 }
106
107 private long fill(final long pOffset, final long pNewOffset, final char pFill) throws IOException {
108 final long diff = pNewOffset - pOffset;
109
110 if (diff > 0) {
111 for (int i = 0; i < diff; i++) {
112 write(pFill);
113 }
114 }
115
116 return pNewOffset;
117 }
118
119 @Override
120 public void finish() throws IOException {
121 if (haveUnclosedEntry) {
122 throw new IOException("This archive contains unclosed entries.");
123 }
124 if (finished) {
125 throw new IOException("This archive has already been finished");
126 }
127 finished = true;
128 }
129
130 @Override
131 public void putArchiveEntry(final ArArchiveEntry entry) throws IOException {
132 if (finished) {
133 throw new IOException("Stream has already been finished");
134 }
135
136 if (prevEntry == null) {
137 writeArchiveHeader();
138 } else {
139 if (prevEntry.getLength() != entryOffset) {
140 throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
141 }
142
143 if (haveUnclosedEntry) {
144 closeArchiveEntry();
145 }
146 }
147
148 prevEntry = entry;
149
150 writeEntryHeader(entry);
151
152 entryOffset = 0;
153 haveUnclosedEntry = true;
154 }
155
156
157
158
159
160
161
162
163 public void setLongFileMode(final int longFileMode) {
164 this.longFileMode = longFileMode;
165 }
166
167 @Override
168 public void write(final byte[] b, final int off, final int len) throws IOException {
169 out.write(b, off, len);
170 count(len);
171 entryOffset += len;
172 }
173
174 private long write(final String data) throws IOException {
175 final byte[] bytes = data.getBytes(US_ASCII);
176 write(bytes);
177 return bytes.length;
178 }
179
180 private void writeArchiveHeader() throws IOException {
181 final byte[] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
182 out.write(header);
183 }
184
185 private void writeEntryHeader(final ArArchiveEntry entry) throws IOException {
186
187 long offset = 0;
188 boolean mustAppendName = false;
189
190 final String n = entry.getName();
191 final int nLength = n.length();
192 if (LONGFILE_ERROR == longFileMode && nLength > 16) {
193 throw new IOException("File name too long, > 16 chars: " + n);
194 }
195 if (LONGFILE_BSD == longFileMode && (nLength > 16 || n.contains(" "))) {
196 mustAppendName = true;
197 offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength);
198 } else {
199 offset += write(n);
200 }
201
202 offset = fill(offset, 16, ' ');
203 final String m = "" + entry.getLastModified();
204 if (m.length() > 12) {
205 throw new IOException("Last modified too long");
206 }
207 offset += write(m);
208
209 offset = fill(offset, 28, ' ');
210 final String u = "" + entry.getUserId();
211 if (u.length() > 6) {
212 throw new IOException("User id too long");
213 }
214 offset += write(u);
215
216 offset = fill(offset, 34, ' ');
217 final String g = "" + entry.getGroupId();
218 if (g.length() > 6) {
219 throw new IOException("Group id too long");
220 }
221 offset += write(g);
222
223 offset = fill(offset, 40, ' ');
224 final String fm = "" + Integer.toString(entry.getMode(), 8);
225 if (fm.length() > 8) {
226 throw new IOException("Filemode too long");
227 }
228 offset += write(fm);
229
230 offset = fill(offset, 48, ' ');
231 final String s = String.valueOf(entry.getLength() + (mustAppendName ? nLength : 0));
232 if (s.length() > 10) {
233 throw new IOException("Size too long");
234 }
235 offset += write(s);
236
237 offset = fill(offset, 58, ' ');
238
239 offset += write(ArArchiveEntry.TRAILER);
240
241 if (mustAppendName) {
242 offset += write(n);
243 }
244
245 }
246 }