Skip to content

Commit 261ac2c

Browse files
authored
prevent null date in Subversion history entry (#4564)
- propagate stream processing error as -1 exit code - fix various style issues fixes #3111
1 parent ad96584 commit 261ac2c

File tree

4 files changed

+113
-78
lines changed

4 files changed

+113
-78
lines changed

opengrok-indexer/src/main/java/org/opengrok/indexer/history/SubversionHistoryParser.java

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919

2020
/*
21-
* Copyright (c) 2006, 2023, Oracle and/or its affiliates. All rights reserved.
21+
* Copyright (c) 2006, 2024, Oracle and/or its affiliates. All rights reserved.
2222
* Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
2323
* Portions Copyright (c) 2020, 2023, Ric Harris <harrisric@users.noreply.github.com>.
2424
*/
@@ -37,6 +37,8 @@
3737
import java.util.List;
3838
import java.util.Map;
3939
import java.util.Map.Entry;
40+
import java.util.Objects;
41+
import java.util.Optional;
4042
import java.util.Set;
4143
import java.util.logging.Level;
4244
import java.util.logging.Logger;
@@ -45,6 +47,7 @@
4547
import javax.xml.parsers.SAXParser;
4648
import javax.xml.parsers.SAXParserFactory;
4749

50+
import org.jetbrains.annotations.VisibleForTesting;
4851
import org.opengrok.indexer.configuration.CommandTimeoutType;
4952
import org.opengrok.indexer.configuration.RuntimeEnvironment;
5053
import org.opengrok.indexer.logger.LoggerFactory;
@@ -111,12 +114,12 @@ public void startElement(String uri, String localName, String qname, Attributes
111114
entry.setActive(true);
112115
entry.setRevision(attr.getValue("revision"));
113116
} else if ("path".equals(qname) && attr.getIndex(COPYFROM_PATH) != -1) {
114-
LOGGER.log(Level.FINER, "rename for {0}", attr.getValue(COPYFROM_PATH));
115-
if ("dir".equals(attr.getValue("kind"))) {
116-
isRenamedDir = true;
117-
} else {
118-
isRenamedFile = true;
119-
}
117+
LOGGER.log(Level.FINER, "rename for {0}", attr.getValue(COPYFROM_PATH));
118+
if ("dir".equals(attr.getValue("kind"))) {
119+
isRenamedDir = true;
120+
} else {
121+
isRenamedFile = true;
122+
}
120123
}
121124
sb.setLength(0);
122125
}
@@ -131,8 +134,8 @@ public void endElement(String uri, String localName, String qname) throws SAXExc
131134
// need to strip microseconds off - assume final character is Z otherwise invalid anyway.
132135
String dateString = s;
133136
if (s.length() > SVN_MILLIS_DATE_LENGTH) {
134-
dateString = dateString.substring(0, SVN_MILLIS_DATE_LENGTH - 1) +
135-
dateString.charAt(dateString.length() - 1);
137+
dateString = dateString.substring(0, SVN_MILLIS_DATE_LENGTH - 1) +
138+
dateString.charAt(dateString.length() - 1);
136139
}
137140
entry.setDate(repository.parse(dateString));
138141
} catch (ParseException ex) {
@@ -153,7 +156,7 @@ public void endElement(String uri, String localName, String qname) throws SAXExc
153156
renamedFiles.add(path.intern());
154157
}
155158
if (isRenamedDir) {
156-
renamedToDirectoryRevisions.put(path.intern(), entry.getRevision());
159+
renamedToDirectoryRevisions.put(path.intern(), entry.getRevision());
157160
}
158161
} else {
159162
LOGGER.log(Level.FINER, "Skipping file ''{0}'' outside repository ''{1}''",
@@ -163,6 +166,11 @@ public void endElement(String uri, String localName, String qname) throws SAXExc
163166
entry.setMessage(s);
164167
}
165168
if ("logentry".equals(qname)) {
169+
// Avoid adding incomplete history entries.
170+
if (Objects.isNull(entry.getDate())) {
171+
throw new SAXException(String.format("date is null in history entry for revision %s",
172+
Optional.ofNullable(entry.getRevision()).orElse("<unknown>")));
173+
}
166174
entries.add(entry);
167175
}
168176
sb.setLength(0);
@@ -209,17 +217,15 @@ History parse(File file, SubversionRepository repos, String sinceRevision,
209217

210218
Executor executor;
211219
try {
212-
executor = repos.getHistoryLogExecutor(file, sinceRevision,
213-
numEntries, cmdType);
220+
executor = repos.getHistoryLogExecutor(file, sinceRevision, numEntries, cmdType);
214221
} catch (IOException e) {
215-
throw new HistoryException("Failed to get history for: \"" +
216-
file.getAbsolutePath() + "\"", e);
222+
throw new HistoryException(String.format("Failed to get history for '%s'", file.getAbsolutePath()), e);
217223
}
218224

219225
int status = executor.exec(true, this);
220226
if (status != 0) {
221-
throw new HistoryException("Failed to get history for: \"" +
222-
file.getAbsolutePath() + "\" Exit code: " + status);
227+
throw new HistoryException(String.format("Failed to get history for '%s': Exit code: %d",
228+
file.getAbsolutePath(), status));
223229
}
224230

225231
List<HistoryEntry> entries = handler.entries;
@@ -266,12 +272,13 @@ public void processStream(InputStream input) throws IOException {
266272
}
267273

268274
/**
269-
* Parse the given string.
275+
* Parse the given string. Only used in tests.
270276
*
271277
* @param buffer The string to be parsed
272278
* @return The parsed history
273279
* @throws IOException if we fail to parse the buffer
274280
*/
281+
@VisibleForTesting
275282
History parse(String buffer) throws IOException {
276283
handler = new Handler("/", "", 0, new SubversionRepository());
277284
processStream(new ByteArrayInputStream(buffer.getBytes(StandardCharsets.UTF_8)));

opengrok-indexer/src/main/java/org/opengrok/indexer/history/SubversionRepository.java

Lines changed: 33 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.ArrayList;
3535
import java.util.HashSet;
3636
import java.util.List;
37+
import java.util.Objects;
3738
import java.util.Set;
3839
import java.util.function.Supplier;
3940
import java.util.logging.Level;
@@ -44,6 +45,7 @@
4445
import javax.xml.parsers.DocumentBuilderFactory;
4546
import javax.xml.parsers.ParserConfigurationException;
4647

48+
import org.jetbrains.annotations.Nullable;
4749
import org.jetbrains.annotations.VisibleForTesting;
4850
import org.opengrok.indexer.configuration.CommandTimeoutType;
4951
import org.opengrok.indexer.configuration.RuntimeEnvironment;
@@ -57,10 +59,10 @@
5759

5860
/**
5961
* Access to a Subversion repository.
60-
*
62+
* <p>
6163
* <b>TODO</b> The current implementation does <b>not</b> support nested
6264
* repositories as described in http://svnbook.red-bean.com/en/1.0/ch07s03.html
63-
*
65+
* </p>
6466
* @author Trond Norbye
6567
*/
6668
public class SubversionRepository extends Repository {
@@ -120,11 +122,10 @@ private String getValue(Node node) {
120122
}
121123

122124
/**
123-
* Get {@code Document} corresponding to the parsed XML output from
124-
* {@code svn info} command.
125-
* @return document with data from {@code info} or null if the {@code svn}
126-
* command failed
125+
* Get {@code Document} corresponding to the parsed XML output from {@code svn info} command.
126+
* @return document with data from {@code info} or null if the {@code svn} command failed
127127
*/
128+
@Nullable
128129
private Document getInfoDocument() {
129130
Document document = null;
130131
List<String> cmd = new ArrayList<>();
@@ -146,26 +147,21 @@ private Document getInfoDocument() {
146147
DocumentBuilder builder = factory.newDocumentBuilder();
147148
document = builder.parse(executor.getOutputStream());
148149
} catch (SAXException saxe) {
149-
LOGGER.log(Level.WARNING,
150-
"Parser error parsing svn output", saxe);
150+
LOGGER.log(Level.WARNING, "Parser error parsing svn output", saxe);
151151
} catch (ParserConfigurationException pce) {
152-
LOGGER.log(Level.WARNING,
153-
"Parser configuration error parsing svn output", pce);
152+
LOGGER.log(Level.WARNING, "Parser configuration error parsing svn output", pce);
154153
} catch (IOException ioe) {
155-
LOGGER.log(Level.WARNING,
156-
"IOException reading from svn process", ioe);
154+
LOGGER.log(Level.WARNING, "IOException reading from svn process", ioe);
157155
}
158156
} else {
159-
LOGGER.log(Level.WARNING,
160-
"Failed to execute svn info for [{0}]. Repository disabled.",
161-
getDirectoryName());
157+
LOGGER.log(Level.WARNING, "Failed to execute svn info for ''{0}''", getDirectoryName());
162158
}
163159

164160
return document;
165161
}
166162

167163
/**
168-
* Get value of given tag in 'svn info' document.
164+
* Get value of given tag in @{code svn info} document.
169165
* @param document document object containing {@code info} contents
170166
* @param tagName name of the tag to return value for
171167
* @return value string
@@ -187,7 +183,7 @@ public void setDirectoryName(File directory) {
187183
String url = getInfoPart(document, URL_ATTR);
188184
if (url == null) {
189185
LOGGER.log(Level.WARNING,
190-
"svn info did not contain an URL for [{0}]. Assuming remote repository.",
186+
"svn info did not contain an URL for ''{0}''. Assuming remote repository.",
191187
getDirectoryName());
192188
setRemote(true);
193189
} else {
@@ -196,8 +192,7 @@ public void setDirectoryName(File directory) {
196192
}
197193
}
198194

199-
String root
200-
= getValue(document.getElementsByTagName("root").item(0));
195+
String root = getValue(document.getElementsByTagName("root").item(0));
201196
if (url != null && root != null) {
202197
reposPath = url.substring(root.length());
203198
rootFound = Boolean.TRUE;
@@ -243,7 +238,7 @@ Executor getHistoryLogExecutor(final File file, String sinceRevision,
243238
// fetch the unneeded revision and remove it later.
244239
cmd.add("BASE:" + sinceRevision);
245240
}
246-
if (filename.length() > 0) {
241+
if (!filename.isEmpty()) {
247242
cmd.add(escapeFileName(filename));
248243
}
249244

@@ -260,8 +255,7 @@ boolean getHistoryGet(OutputStream out, String parent, String basename, String r
260255
try {
261256
filepath = new File(parent, basename).getCanonicalPath();
262257
} catch (IOException exp) {
263-
LOGGER.log(Level.SEVERE,
264-
"Failed to get canonical path: {0}", exp.getClass());
258+
LOGGER.log(Level.SEVERE, "Failed to get canonical path: {0}", exp.getClass());
265259
return false;
266260
}
267261
String filename = filepath.substring(getDirectoryName().length() + 1);
@@ -280,11 +274,13 @@ boolean getHistoryGet(OutputStream out, String parent, String basename, String r
280274
RuntimeEnvironment.getInstance().getInteractiveCommandTimeout());
281275
if (executor.exec() == 0) {
282276
try {
283-
copyBytes(out::write, executor.getOutputStream());
277+
InputStream stream = executor.getOutputStream();
278+
if (!Objects.isNull(stream)) {
279+
copyBytes(out::write, stream);
280+
}
284281
return true;
285282
} catch (IOException e) {
286-
LOGGER.log(Level.SEVERE, "Failed to get content for {0}",
287-
basename);
283+
LOGGER.log(Level.SEVERE, "Failed to get content for ''{0}''", basename);
288284
}
289285
}
290286

@@ -318,35 +314,28 @@ private Executor getDirectoryListExecutor(final String dir, String atRevision, C
318314
* Provides a list of files that were in a directory at a given revision.
319315
* This is useful for finding files that will need special renamed file history
320316
* handling because they were in a directory when it was renamed.
321-
*
317+
* <p>
322318
* Note that this doesn't throw an exception even if the command was not completed
323319
* because we will still be able to get the file history up to this point.
324-
*
320+
* </p>
325321
* @param directory the directory to check
326322
* @param revision the revision to check at
327323
* @param cmdType the timeout setting.
328324
* @return the files that were in the directory at that revision
329325
*/
330-
Set<String> getFilesInDirectoryAtRevision(String directory, String revision,
331-
CommandTimeoutType cmdType) {
326+
Set<String> getFilesInDirectoryAtRevision(String directory, String revision, CommandTimeoutType cmdType) {
332327

333328
Executor executor = getDirectoryListExecutor(
334329
RuntimeEnvironment.getInstance().getSourceRootPath() + File.separator + directory,
335330
revision, cmdType);
336331

337332
Set<String> files = new HashSet<>();
338333

339-
StreamHandler directoryLogStreamHandler = new StreamHandler() {
340-
341-
@Override
342-
public void processStream(InputStream in) throws IOException {
343-
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
344-
.lines()
345-
.filter(s -> !s.isBlank())
346-
.map(s -> directory + File.separator + s)
347-
.forEach(files::add);
348-
}
349-
};
334+
StreamHandler directoryLogStreamHandler = in -> new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
335+
.lines()
336+
.filter(s -> !s.isBlank())
337+
.map(s -> directory + File.separator + s)
338+
.forEach(files::add);
350339

351340
int status = executor.exec(true, directoryLogStreamHandler);
352341
if (status != 0) {
@@ -373,15 +362,14 @@ History getHistory(File file, String sinceRevision) throws HistoryException {
373362
return getHistory(file, sinceRevision, 0, CommandTimeoutType.INDEXER);
374363
}
375364

376-
private History getHistory(File file, String sinceRevision, int numEntries,
377-
CommandTimeoutType cmdType)
365+
private History getHistory(File file, String sinceRevision, int numEntries, CommandTimeoutType cmdType)
378366
throws HistoryException {
379367
return new SubversionHistoryParser().parse(file, this, sinceRevision,
380368
numEntries, cmdType);
381369
}
382370

383371
private String escapeFileName(String name) {
384-
if (name.length() == 0) {
372+
if (name.isEmpty()) {
385373
return name;
386374
}
387375
return name + "@";
@@ -423,7 +411,7 @@ public boolean fileHasAnnotation(File file) {
423411

424412
@Override
425413
public boolean fileHasHistory(File file) {
426-
// @TODO: Research how to cheaply test if a file in a given
414+
// TODO: Research how to cheaply test if a file in a given
427415
// SVN repo has history. If there is a cheap test, then this
428416
// code can be refined, boosting performance.
429417
return true;
@@ -517,7 +505,7 @@ public String determineCurrentVersion(CommandTimeoutType cmdType) throws IOExcep
517505
}
518506
}
519507
} catch (HistoryException ex) {
520-
LOGGER.log(Level.WARNING, "cannot get current version info for {0}",
508+
LOGGER.log(Level.WARNING, "cannot get current version info for ''{0}''",
521509
getDirectoryName());
522510
}
523511

0 commit comments

Comments
 (0)