1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Copyright (c) 2007, 2022, Oracle and/or its affiliates. All rights reserved. 22 */ 23 package org.opengrok.indexer.history; 24 25 import java.io.BufferedReader; 26 import java.io.File; 27 import java.io.FileReader; 28 import java.io.IOException; 29 import java.io.Reader; 30 import java.nio.file.Paths; 31 import java.text.ParseException; 32 import java.util.ArrayList; 33 import java.util.Date; 34 35 import org.jetbrains.annotations.Nullable; 36 import org.opengrok.indexer.util.IOUtils; 37 38 /** 39 * Reads and filters out junk from a SCCS history file. 40 * See sccsfile(4) for details of the file format. 41 * Wrote it since invoking {@code sccs prs} for each file was 42 * taking a lot of time. Time to index history has reduced 4 to 1! 43 */ 44 final class SCCSHistoryParser { 45 46 static final String SCCS_DIR_NAME = "SCCS"; 47 48 boolean pass; 49 boolean passRecord; 50 boolean active; 51 int field; 52 boolean sep; 53 StringBuilder sccsRecord = new StringBuilder(128); 54 Reader in; 55 56 // Record fields 57 private String revision; 58 private Date rdate; 59 private String author; 60 private String comment; 61 private final SCCSRepository repository; 62 SCCSHistoryParser(SCCSRepository repository)63 SCCSHistoryParser(SCCSRepository repository) { 64 this.repository = repository; 65 } 66 parse(File file)67 History parse(File file) throws HistoryException { 68 try { 69 return parseFile(file); 70 } catch (IOException e) { 71 throw new HistoryException("Failed to get history for " + 72 "\"" + file.getAbsolutePath() + "\":", e); 73 } catch (ParseException e) { 74 throw new HistoryException("Failed to parse history for " + 75 "\"" + file.getAbsolutePath() + "\":", e); 76 } 77 } 78 parseFile(File file)79 private History parseFile(File file) throws IOException, ParseException { 80 File f = getSCCSFile(file); 81 if (f == null) { 82 return null; 83 } 84 85 in = new BufferedReader(new FileReader(getSCCSFile(file))); 86 pass = sep = false; 87 passRecord = true; 88 active = true; 89 field = 0; 90 91 ArrayList<HistoryEntry> entries = new ArrayList<>(); 92 while (next()) { 93 HistoryEntry entry = new HistoryEntry(); 94 entry.setRevision(getRevision()); 95 entry.setDate(getDate()); 96 entry.setAuthor(getAuthor()); 97 entry.setMessage(getComment()); 98 entry.setActive(isActive()); 99 entries.add(entry); 100 } 101 102 IOUtils.close(in); 103 104 History history = new History(); 105 history.setHistoryEntries(entries); 106 return history; 107 } 108 109 /** 110 * Read a single line of delta record into the {@link #sccsRecord} member. 111 * 112 * @throws java.io.IOException on I/O error 113 * @return boolean indicating whether there is another record. 114 */ next()115 private boolean next() throws java.io.IOException { 116 sep = true; 117 sccsRecord.setLength(0); 118 int c; 119 while ((c = read()) > 1) { 120 sccsRecord.append((char) c); 121 } 122 // to flag that revision needs to be re populated if you really need it 123 revision = null; 124 return (sccsRecord.length() > 2); 125 } 126 127 /** 128 * Split record into fields. 129 * 130 * @throws ParseException if the date in the record cannot be parsed 131 */ initFields()132 private void initFields() throws ParseException { 133 if (revision == null) { 134 String[] f = sccsRecord.toString().split(" ", 6); 135 if (f.length > 5) { 136 revision = f[1]; 137 try { 138 rdate = repository.parse(f[2] + " " + f[3]); 139 } catch (ParseException e) { 140 rdate = null; 141 // 142 // Throw new exception up so that it can be paired with filename 143 // on which the problem occurred. 144 // 145 throw e; 146 } 147 author = f[4]; 148 comment = f[5]; 149 } else { 150 rdate = null; 151 author = null; 152 comment = null; 153 } 154 } 155 } 156 157 /** 158 * @return get the revision string of current log record 159 */ getRevision()160 private String getRevision() throws ParseException { 161 initFields(); 162 return revision; 163 } 164 165 /** 166 * @return get the date associated with current log record 167 */ getDate()168 private Date getDate() throws ParseException { 169 initFields(); 170 return rdate; 171 } 172 173 /** 174 * @return get the author of current log record 175 */ getAuthor()176 private String getAuthor() throws ParseException { 177 initFields(); 178 return author; 179 } 180 /** 181 * @return get the comments of current log record 182 */ getComment()183 private String getComment() throws ParseException { 184 initFields(); 185 return comment; 186 } 187 isActive()188 private boolean isActive() { 189 return active; 190 } 191 read()192 private int read() throws java.io.IOException { 193 int c, d, dt; 194 while ((c = in.read()) != -1) { 195 switch (c) { //NOPMD 196 case 1: 197 d = in.read(); 198 switch (d) { 199 case 'c': 200 case 't': 201 case 'u': 202 d = in.read(); 203 if (d != ' ') { 204 return (d); 205 } 206 pass = true; 207 break; 208 case 'd': 209 d = in.read(); 210 if (d == ' ') { 211 dt = in.read(); 212 active = dt != 'R'; 213 passRecord = true; 214 field = 1; 215 } else { 216 return (d); 217 } 218 break; 219 case -1: 220 case 'I': //the file contents start 221 case 'D': 222 case 'E': 223 case 'T': 224 return -1; 225 case 'e': 226 pass = false; 227 if (sep && passRecord) { 228 return 1; 229 } 230 passRecord = true; 231 break; 232 default: 233 pass = false; 234 } 235 break; 236 case ' ': 237 if (passRecord) { 238 if (field > 0) { 239 field++; 240 pass = true; 241 } 242 if (field > 5) { 243 field = 0; 244 pass = false; 245 return c; 246 } 247 } 248 default: 249 if (pass && passRecord) { 250 return c; 251 } 252 } 253 } 254 return -1; 255 } 256 getSCCSFile(File file)257 private static File getSCCSFile(File file) { 258 return getSCCSFile(file.getParent(), file.getName()); 259 } 260 261 @Nullable getSCCSFile(String parent, String name)262 static File getSCCSFile(String parent, String name) { 263 File f = Paths.get(parent, SCCS_DIR_NAME, "s." + name).toFile(); 264 if (!f.exists()) { 265 return null; 266 } 267 return f; 268 } 269 } 270