* If the resolution fails, report an error.
*
@@ -884,15 +887,8 @@ public OutputStream compress(OutputStream out) {
},
GZIP {
@Override
- public InputStream extract(InputStream _in) throws IOException {
- HeadBufferingStream in = new HeadBufferingStream(_in, SIDE_BUFFER_SIZE);
- try {
- return new GZIPInputStream(in, 8192, true);
- } catch (IOException e) {
- // various people reported "java.io.IOException: Not in GZIP format" here, so diagnose this problem better
- in.fillSide();
- throw new IOException(e.getMessage() + "\nstream=" + Util.toHexString(in.getSideBuffer()), e);
- }
+ public InputStream extract(InputStream in) throws IOException {
+ return new GZIPInputStream(new BufferedInputStream(in));
}
@Override
@@ -956,7 +952,7 @@ public Void invoke(File dir, VirtualChannel channel) throws IOException {
*
*
* @param archive
- * The resource that represents the tgz/zip file. This URL must support the {@code Last-Modified} header.
+ * The resource that represents the tgz/zip file. This URL must support the {@code Last-Modified} header or the {@code ETag} header.
* (For example, you could use {@link ClassLoader#getResource}.)
* @param listener
* If non-null, a message will be printed to this listener once this method decides to
@@ -978,12 +974,18 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen
try {
FilePath timestamp = this.child(".timestamp");
long lastModified = timestamp.lastModified();
+ // https://httpwg.org/specs/rfc9110.html#field.etag is the ETag specification
+ // Read previously stored ETag if timestamp is available
+ String etag = timestamp.exists() ? fixEmptyAndTrim(timestamp.readToString()) : null;
URLConnection con;
try {
con = ProxyConfiguration.open(archive);
if (lastModified != 0) {
con.setIfModifiedSince(lastModified);
}
+ if (etag != null) {
+ con.setRequestProperty("If-None-Match", etag);
+ }
con.connect();
} catch (IOException x) {
if (this.exists()) {
@@ -995,8 +997,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen
}
}
- if (con instanceof HttpURLConnection) {
- HttpURLConnection httpCon = (HttpURLConnection) con;
+ if (con instanceof HttpURLConnection httpCon) {
int responseCode = httpCon.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
@@ -1010,7 +1011,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen
return false;
}
}
- if (lastModified != 0) {
+ if (lastModified != 0 || etag != null) {
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
return false;
} else if (responseCode != HttpURLConnection.HTTP_OK) {
@@ -1021,8 +1022,12 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen
}
long sourceTimestamp = con.getLastModified();
+ String resultEtag = fixEmptyAndTrim(con.getHeaderField("ETag"));
if (this.exists()) {
+ if (equalETags(etag, resultEtag)) {
+ return false; // already up to date
+ }
if (lastModified != 0 && sourceTimestamp == lastModified)
return false; // already up to date
this.deleteContents();
@@ -1036,6 +1041,10 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen
// First try to download from the agent machine.
try {
act(new Unpack(archive));
+ if (resultEtag != null && !equalETags(etag, resultEtag)) {
+ /* Store the ETag value in the timestamp file for later use */
+ timestamp.write(resultEtag, "UTF-8");
+ }
timestamp.touch(sourceTimestamp);
return true;
} catch (IOException x) {
@@ -1050,11 +1059,15 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen
if (archive.toExternalForm().endsWith(".zip"))
unzipFrom(cis);
else
- untarFrom(cis, GZIP);
+ untarFrom(cis, TarCompression.GZIP);
} catch (IOException e) {
throw new IOException(String.format("Failed to unpack %s (%d bytes read of total %d)",
archive, cis.getByteCount(), con.getContentLength()), e);
}
+ if (resultEtag != null && !equalETags(etag, resultEtag)) {
+ /* Store the ETag value in the timestamp file for later use */
+ timestamp.write(resultEtag, "UTF-8");
+ }
timestamp.touch(sourceTimestamp);
return true;
} catch (IOException e) {
@@ -1062,6 +1075,25 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen
}
}
+ /* Return true if etag1 equals etag2 as defined by the etag specification
+ https://httpwg.org/specs/rfc9110.html#field.etag
+ */
+ private boolean equalETags(String etag1, String etag2) {
+ if (etag1 == null || etag2 == null) {
+ return false;
+ }
+ if (etag1.equals(etag2)) {
+ return true;
+ }
+ /* Weak tags are identified by leading characters "W/" as a marker */
+ /* Weak tag marker must not be considered in tag comparison.
+ This implements the weak comparison in the specification at
+ https://httpwg.org/specs/rfc9110.html#field.etag */
+ String opaqueTag1 = etag1.startsWith("W/") ? etag1.substring(2) : etag1;
+ String opaqueTag2 = etag2.startsWith("W/") ? etag2.substring(2) : etag2;
+ return opaqueTag1.equals(opaqueTag2);
+ }
+
// this reads from arbitrary URL
private static final class Unpack extends MasterToSlaveFileCallable
+ * Input example 1: !"£$%^&*()_+}{:@~?><|¬`,./;'#[]- =
+ * Output example 1: !"%C2%A3$%^&*()_+}{:@~?><|%C2%AC`,./;'#[]-%20=
+ *
+ * Notes:
+ *
+ *
+ */
public static String encode(String s) {
return Util.encode(s);
}
@@ -766,6 +831,13 @@ public static String encode(String s) {
* Shortcut function for calling {@link URLEncoder#encode(String,String)} (with UTF-8 encoding).
* Useful for encoding URL query parameters in jelly code (as in {@code "...?param=${h.urlEncode(something)}"}).
* For convenience in jelly code, it also accepts null parameter, and then returns an empty string.
+ *
+ * Input example 1: & " ' < >
+ * Output example 1: %26+%22+%27+%3C+%3E
+ * Input example 2: !"£$%^&*()_+}{:@~?><|¬`,./;'#[]-=
+ * Output example 2: %21%22%C2%A3%24%25%5E%26*%28%29_%2B%7D%7B%3A%40%7E%3F%3E%3C%7C%C2%AC%60%2C.%2F%3B%27%23%5B%5D-%3D
+ *
+ * Note: A blank space will render as + (You can see this in above examples)
*
* @since 2.200
*/
@@ -776,10 +848,31 @@ public static String urlEncode(String s) {
return URLEncoder.encode(s, StandardCharsets.UTF_8);
}
+ /**
+ * Transforms the input string so it renders as written in HTML output: newlines are converted to HTML line breaks, consecutive spaces are retained as {@code }, and HTML metacharacters are escaped.
+ *
+ * Input example 1: & " ' < >
+ * Output example 1: & " ' < >
+ * Input example 2: !"£$%^&*()_+}{:@~?><|¬`,./;'#[]-=
+ * Output example 2: !"£$%^&*()_+}{:@~?><|¬`,./;'#[]-=
+ *
+ * @see #xmlEscape
+ * @see hudson.Util#escape
+ */
public static String escape(String s) {
return Util.escape(s);
}
+ /**
+ * Escapes XML unsafe characters
+ *
+ * Input example 1: < > &
+ * Output example 1: < > &
+ * Input example 2: !"£$%^&*()_+}{:@~?><|¬`,./;'#[]-=
+ * Output example 2: !"£$%^&*()_+}{:@~?><|¬`,./;'#[]-=
+ *
+ * @see hudson.Util#xmlEscape
+ */
public static String xmlEscape(String s) {
return Util.xmlEscape(s);
}
@@ -788,6 +881,16 @@ public static String xmlUnescape(String s) {
return s.replace("<", "<").replace(">", ">").replace("&", "&");
}
+ /**
+ * Escapes a string so it can be used in an HTML attribute value.
+ *
+ * Input example 1: & " ' < >
+ * Output example 1: & " ' < >
+ * Input example 2: !"£$%^&*()_+}{:@~?><|¬`,./;'#[]-=
+ * Output example 2: !"£$%^&*()_+}{:@~?><|¬`,./;'#[]-=
+ *
+ * Note: 2 consecutive blank spaces will not render any special chars.
+ */
public static String htmlAttributeEscape(String text) {
StringBuilder buf = new StringBuilder(text.length() + 64);
for (int i = 0; i < text.length(); i++) {
@@ -834,7 +937,7 @@ public static void checkPermission(Object object, Permission permission) throws
if (object instanceof AccessControlled)
checkPermission((AccessControlled) object, permission);
else {
- List