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 private static final char PAD = '\n';
40
41 private static final char SPACE = ' ';
42
43
44 public static final int LONGFILE_ERROR = 0;
45
46
47 public static final int LONGFILE_BSD = 1;
48
49 private long entryOffset;
50 private int headerPlus;
51 private ArArchiveEntry prevEntry;
52 private boolean prevEntryOpen;
53 private int longFileMode = LONGFILE_ERROR;
54
55 public ArArchiveOutputStream(final OutputStream out) {
56 super(out);
57 }
58
59 private String checkLength(final String value, final int max, final String name) throws IOException {
60 if (value.length() > max) {
61 throw new IOException(name + " too long");
62 }
63 return value;
64 }
65
66
67
68
69 @Override
70 public void close() throws IOException {
71 try {
72 if (!isFinished()) {
73 finish();
74 }
75 } finally {
76 prevEntry = null;
77 super.close();
78 }
79 }
80
81 @Override
82 public void closeArchiveEntry() throws IOException {
83 checkFinished();
84 if (prevEntry == null || !prevEntryOpen) {
85 throw new IOException("No current entry to close");
86 }
87 if ((headerPlus + entryOffset) % 2 != 0) {
88 out.write(PAD);
89 }
90 prevEntryOpen = false;
91 }
92
93 @Override
94 public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
95 checkFinished();
96 return new ArArchiveEntry(inputFile, entryName);
97 }
98
99
100
101
102
103
104 @Override
105 public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
106 checkFinished();
107 return new ArArchiveEntry(inputPath, entryName, options);
108 }
109
110 @Override
111 public void finish() throws IOException {
112 if (prevEntryOpen) {
113 throw new IOException("This archive contains unclosed entries.");
114 }
115 checkFinished();
116 super.finish();
117 }
118
119 private int pad(final int offset, final int newOffset, final char fill) throws IOException {
120 final int diff = newOffset - offset;
121 if (diff > 0) {
122 for (int i = 0; i < diff; i++) {
123 write(fill);
124 }
125 }
126 return newOffset;
127 }
128
129 @Override
130 public void putArchiveEntry(final ArArchiveEntry entry) throws IOException {
131 checkFinished();
132 if (prevEntry == null) {
133 writeArchiveHeader();
134 } else {
135 if (prevEntry.getLength() != entryOffset) {
136 throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
137 }
138 if (prevEntryOpen) {
139 closeArchiveEntry();
140 }
141 }
142 prevEntry = entry;
143 headerPlus = writeEntryHeader(entry);
144 entryOffset = 0;
145 prevEntryOpen = true;
146 }
147
148
149
150
151
152
153
154
155 public void setLongFileMode(final int longFileMode) {
156 this.longFileMode = longFileMode;
157 }
158
159 @Override
160 public void write(final byte[] b, final int off, final int len) throws IOException {
161 out.write(b, off, len);
162 count(len);
163 entryOffset += len;
164 }
165
166 private int write(final String data) throws IOException {
167 final byte[] bytes = data.getBytes(US_ASCII);
168 write(bytes);
169 return bytes.length;
170 }
171
172 private void writeArchiveHeader() throws IOException {
173 out.write(ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER));
174 }
175
176 private int writeEntryHeader(final ArArchiveEntry entry) throws IOException {
177 int offset = 0;
178 boolean appendName = false;
179 final String eName = entry.getName();
180 final int nLength = eName.length();
181 if (LONGFILE_ERROR == longFileMode && nLength > 16) {
182 throw new IOException("File name too long, > 16 chars: " + eName);
183 }
184 if (LONGFILE_BSD == longFileMode && (nLength > 16 || eName.indexOf(SPACE) > -1)) {
185 appendName = true;
186 final String fileNameLen = ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength;
187 if (fileNameLen.length() > 16) {
188 throw new IOException("File length too long, > 16 chars: " + eName);
189 }
190 offset += write(fileNameLen);
191 } else {
192 offset += write(eName);
193 }
194 offset = pad(offset, 16, SPACE);
195
196 offset += write(checkLength(String.valueOf(entry.getLastModified()), 12, "Last modified"));
197 offset = pad(offset, 28, SPACE);
198
199 offset += write(checkLength(String.valueOf(entry.getUserId()), 6, "User ID"));
200 offset = pad(offset, 34, SPACE);
201
202 offset += write(checkLength(String.valueOf(entry.getGroupId()), 6, "Group ID"));
203 offset = pad(offset, 40, SPACE);
204
205 offset += write(checkLength(String.valueOf(Integer.toString(entry.getMode(), 8)), 8, "File mode"));
206 offset = pad(offset, 48, SPACE);
207
208
209 offset += write(checkLength(String.valueOf(entry.getLength() + (appendName ? nLength : 0)), 10, "Size"));
210 offset = pad(offset, 58, SPACE);
211 offset += write(ArArchiveEntry.TRAILER);
212
213 if (appendName) {
214 offset += write(eName);
215 }
216 return offset;
217 }
218
219 }