001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.ByteArrayOutputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.io.OutputStreamWriter;
027import java.io.PrintWriter;
028import java.io.StringWriter;
029import java.nio.charset.StandardCharsets;
030import java.util.ArrayList;
031import java.util.List;
032import java.util.Locale;
033
034import com.puppycrawl.tools.checkstyle.api.AuditEvent;
035import com.puppycrawl.tools.checkstyle.api.AuditListener;
036import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
037import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
038
039/**
040 * Simple SARIF logger.
041 * SARIF stands for the static analysis results interchange format.
042 * See <a href="https://sarifweb.azurewebsites.net/">reference</a>
043 */
044public class SarifLogger extends AbstractAutomaticBean implements AuditListener {
045
046    /** The length of unicode placeholder. */
047    private static final int UNICODE_LENGTH = 4;
048
049    /** Unicode escaping upper limit. */
050    private static final int UNICODE_ESCAPE_UPPER_LIMIT = 0x1F;
051
052    /** Input stream buffer size. */
053    private static final int BUFFER_SIZE = 1024;
054
055    /** The placeholder for message. */
056    private static final String MESSAGE_PLACEHOLDER = "${message}";
057
058    /** The placeholder for severity level. */
059    private static final String SEVERITY_LEVEL_PLACEHOLDER = "${severityLevel}";
060
061    /** The placeholder for uri. */
062    private static final String URI_PLACEHOLDER = "${uri}";
063
064    /** The placeholder for line. */
065    private static final String LINE_PLACEHOLDER = "${line}";
066
067    /** The placeholder for column. */
068    private static final String COLUMN_PLACEHOLDER = "${column}";
069
070    /** The placeholder for rule id. */
071    private static final String RULE_ID_PLACEHOLDER = "${ruleId}";
072
073    /** The placeholder for version. */
074    private static final String VERSION_PLACEHOLDER = "${version}";
075
076    /** The placeholder for results. */
077    private static final String RESULTS_PLACEHOLDER = "${results}";
078
079    /** Helper writer that allows easy encoding and printing. */
080    private final PrintWriter writer;
081
082    /** Close output stream in auditFinished. */
083    private final boolean closeStream;
084
085    /** The results. */
086    private final List<String> results = new ArrayList<>();
087
088    /** Content for the entire report. */
089    private final String report;
090
091    /** Content for result representing an error with source line and column. */
092    private final String resultLineColumn;
093
094    /** Content for result representing an error with source line only. */
095    private final String resultLineOnly;
096
097    /** Content for result representing an error with filename only and without source location. */
098    private final String resultFileOnly;
099
100    /** Content for result representing an error without filename or location. */
101    private final String resultErrorOnly;
102
103    /**
104     * Creates a new {@code SarifLogger} instance.
105     *
106     * @param outputStream where to log audit events
107     * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished()
108     * @throws IllegalArgumentException if outputStreamOptions is null
109     * @throws IOException if there is reading errors.
110     * @noinspection deprecation
111     * @noinspectionreason We are forced to keep AutomaticBean compatability
112     *     because of maven-checkstyle-plugin. Until #12873.
113     */
114    public SarifLogger(
115        OutputStream outputStream,
116        AutomaticBean.OutputStreamOptions outputStreamOptions) throws IOException {
117        this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
118    }
119
120    /**
121     * Creates a new {@code SarifLogger} instance.
122     *
123     * @param outputStream where to log audit events
124     * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished()
125     * @throws IllegalArgumentException if outputStreamOptions is null
126     * @throws IOException if there is reading errors.
127     */
128    public SarifLogger(
129        OutputStream outputStream,
130        OutputStreamOptions outputStreamOptions) throws IOException {
131        if (outputStreamOptions == null) {
132            throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
133        }
134        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
135        closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
136        report = readResource("/com/puppycrawl/tools/checkstyle/sarif/SarifReport.template");
137        resultLineColumn =
138            readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineColumn.template");
139        resultLineOnly =
140            readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineOnly.template");
141        resultFileOnly =
142            readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultFileOnly.template");
143        resultErrorOnly =
144            readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultErrorOnly.template");
145    }
146
147    @Override
148    protected void finishLocalSetup() {
149        // No code by default
150    }
151
152    @Override
153    public void auditStarted(AuditEvent event) {
154        // No code by default
155    }
156
157    @Override
158    public void auditFinished(AuditEvent event) {
159        final String version = SarifLogger.class.getPackage().getImplementationVersion();
160        final String rendered = report
161            .replace(VERSION_PLACEHOLDER, String.valueOf(version))
162            .replace(RESULTS_PLACEHOLDER, String.join(",\n", results));
163        writer.print(rendered);
164        if (closeStream) {
165            writer.close();
166        }
167        else {
168            writer.flush();
169        }
170    }
171
172    @Override
173    public void addError(AuditEvent event) {
174        if (event.getColumn() > 0) {
175            results.add(resultLineColumn
176                .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
177                .replace(URI_PLACEHOLDER, event.getFileName())
178                .replace(COLUMN_PLACEHOLDER, Integer.toString(event.getColumn()))
179                .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine()))
180                .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage()))
181                .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey())
182            );
183        }
184        else {
185            results.add(resultLineOnly
186                .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
187                .replace(URI_PLACEHOLDER, event.getFileName())
188                .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine()))
189                .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage()))
190                .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey())
191            );
192        }
193    }
194
195    @Override
196    public void addException(AuditEvent event, Throwable throwable) {
197        final StringWriter stringWriter = new StringWriter();
198        final PrintWriter printer = new PrintWriter(stringWriter);
199        throwable.printStackTrace(printer);
200        if (event.getFileName() == null) {
201            results.add(resultErrorOnly
202                .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
203                .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString()))
204            );
205        }
206        else {
207            results.add(resultFileOnly
208                .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
209                .replace(URI_PLACEHOLDER, event.getFileName())
210                .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString()))
211            );
212        }
213    }
214
215    @Override
216    public void fileStarted(AuditEvent event) {
217        // No need to implement this method in this class
218    }
219
220    @Override
221    public void fileFinished(AuditEvent event) {
222        // No need to implement this method in this class
223    }
224
225    /**
226     * Render the severity level into SARIF severity level.
227     *
228     * @param severityLevel the Severity level.
229     * @return the rendered severity level in string.
230     */
231    private static String renderSeverityLevel(SeverityLevel severityLevel) {
232        final String renderedSeverityLevel;
233        switch (severityLevel) {
234            case IGNORE:
235                renderedSeverityLevel = "none";
236                break;
237            case INFO:
238                renderedSeverityLevel = "note";
239                break;
240            case WARNING:
241                renderedSeverityLevel = "warning";
242                break;
243            case ERROR:
244            default:
245                renderedSeverityLevel = "error";
246                break;
247        }
248        return renderedSeverityLevel;
249    }
250
251    /**
252     * Escape \b, \f, \n, \r, \t, \", \\ and U+0000 through U+001F.
253     * See <a href="https://www.ietf.org/rfc/rfc4627.txt">reference</a> - 2.5. Strings
254     *
255     * @param value the value to escape.
256     * @return the escaped value if necessary.
257     */
258    public static String escape(String value) {
259        final int length = value.length();
260        final StringBuilder sb = new StringBuilder(length);
261        for (int i = 0; i < length; i++) {
262            final char chr = value.charAt(i);
263            switch (chr) {
264                case '"':
265                    sb.append("\\\"");
266                    break;
267                case '\\':
268                    sb.append("\\\\");
269                    break;
270                case '\b':
271                    sb.append("\\b");
272                    break;
273                case '\f':
274                    sb.append("\\f");
275                    break;
276                case '\n':
277                    sb.append("\\n");
278                    break;
279                case '\r':
280                    sb.append("\\r");
281                    break;
282                case '\t':
283                    sb.append("\\t");
284                    break;
285                case '/':
286                    sb.append("\\/");
287                    break;
288                default:
289                    if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) {
290                        sb.append(escapeUnicode1F(chr));
291                    }
292                    else {
293                        sb.append(chr);
294                    }
295                    break;
296            }
297        }
298        return sb.toString();
299    }
300
301    /**
302     * Escape the character between 0x00 to 0x1F in JSON.
303     *
304     * @param chr the character to be escaped.
305     * @return the escaped string.
306     */
307    private static String escapeUnicode1F(char chr) {
308        final String hexString = Integer.toHexString(chr);
309        return "\\u"
310                + "0".repeat(UNICODE_LENGTH - hexString.length())
311                + hexString.toUpperCase(Locale.US);
312    }
313
314    /**
315     * Read string from given resource.
316     *
317     * @param name name of the desired resource
318     * @return the string content from the give resource
319     * @throws IOException if there is reading errors
320     */
321    public static String readResource(String name) throws IOException {
322        try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name);
323             ByteArrayOutputStream result = new ByteArrayOutputStream()) {
324            if (inputStream == null) {
325                throw new IOException("Cannot find the resource " + name);
326            }
327            final byte[] buffer = new byte[BUFFER_SIZE];
328            int length = inputStream.read(buffer);
329            while (length != -1) {
330                result.write(buffer, 0, length);
331                length = inputStream.read(buffer);
332            }
333            return result.toString(StandardCharsets.UTF_8);
334        }
335    }
336}