From e9f02fb4c4e170b225c415dab865cc4799b2b2df Mon Sep 17 00:00:00 2001 From: Yutaka Kamei Date: Tue, 9 Apr 2024 10:02:53 +0900 Subject: [PATCH] Add padding for y-axis by 20% When drawing the chart, if the minimum and maximum values of the y-axis are the same as the actual minimum and maximum values, the transition of the chart tends to be extreme. To avoid this, allow margins for the minimum and maximum values on the y-axis. --- README.md | 4 +- dist/index.js | 307 ++++++++++++++++++++++++++----- src/MermaidXYChart.ts | 12 +- tests/GitHubIssueContent.test.ts | 4 +- tests/MermaidXYChart.test.ts | 6 +- 5 files changed, 279 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 8bd2a6a..88ae2fe 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ config: xychart-beta title "CI (.github/workflows/ci.yml for status=success)" x-axis ["Mar 6", "Mar 9", "Mar 10", "Mar 11", "Mar 12", "Mar 13", "Mar 14", "Mar 15", "Mar 16", "Mar 17", "Mar 21", "Mar 23", "Mar 24", "Mar 28", "Mar 29", "Mar 30", "Apr 3"] - y-axis "Duration (average in seconds)" - bar [38, 39, 37, 42,41, 43, 37, 46, 38,41, 43, 38, 39, 54,41, 42, 69] + y-axis "Duration (average in seconds)" 29 --> 82 + bar [38, 39, 37, 42,41, 43, 37, 46, 38,41, 43, 38, 39, 54, 41, 42, 69] ``` ## Motivation diff --git a/dist/index.js b/dist/index.js index d17b2b4..3755433 100644 --- a/dist/index.js +++ b/dist/index.js @@ -12647,6 +12647,132 @@ function onConnectTimeout (socket) { module.exports = buildConnector +/***/ }), + +/***/ 4462: +/***/ ((module) => { + +"use strict"; + + +/** @type {Record} */ +const headerNameLowerCasedRecord = {} + +// https://developer.mozilla.org/docs/Web/HTTP/Headers +const wellknownHeaderNames = [ + 'Accept', + 'Accept-Encoding', + 'Accept-Language', + 'Accept-Ranges', + 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Origin', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Access-Control-Request-Headers', + 'Access-Control-Request-Method', + 'Age', + 'Allow', + 'Alt-Svc', + 'Alt-Used', + 'Authorization', + 'Cache-Control', + 'Clear-Site-Data', + 'Connection', + 'Content-Disposition', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-Range', + 'Content-Security-Policy', + 'Content-Security-Policy-Report-Only', + 'Content-Type', + 'Cookie', + 'Cross-Origin-Embedder-Policy', + 'Cross-Origin-Opener-Policy', + 'Cross-Origin-Resource-Policy', + 'Date', + 'Device-Memory', + 'Downlink', + 'ECT', + 'ETag', + 'Expect', + 'Expect-CT', + 'Expires', + 'Forwarded', + 'From', + 'Host', + 'If-Match', + 'If-Modified-Since', + 'If-None-Match', + 'If-Range', + 'If-Unmodified-Since', + 'Keep-Alive', + 'Last-Modified', + 'Link', + 'Location', + 'Max-Forwards', + 'Origin', + 'Permissions-Policy', + 'Pragma', + 'Proxy-Authenticate', + 'Proxy-Authorization', + 'RTT', + 'Range', + 'Referer', + 'Referrer-Policy', + 'Refresh', + 'Retry-After', + 'Sec-WebSocket-Accept', + 'Sec-WebSocket-Extensions', + 'Sec-WebSocket-Key', + 'Sec-WebSocket-Protocol', + 'Sec-WebSocket-Version', + 'Server', + 'Server-Timing', + 'Service-Worker-Allowed', + 'Service-Worker-Navigation-Preload', + 'Set-Cookie', + 'SourceMap', + 'Strict-Transport-Security', + 'Supports-Loading-Mode', + 'TE', + 'Timing-Allow-Origin', + 'Trailer', + 'Transfer-Encoding', + 'Upgrade', + 'Upgrade-Insecure-Requests', + 'User-Agent', + 'Vary', + 'Via', + 'WWW-Authenticate', + 'X-Content-Type-Options', + 'X-DNS-Prefetch-Control', + 'X-Frame-Options', + 'X-Permitted-Cross-Domain-Policies', + 'X-Powered-By', + 'X-Requested-With', + 'X-XSS-Protection' +] + +for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = wellknownHeaderNames[i] + const lowerCasedKey = key.toLowerCase() + headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = + lowerCasedKey +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(headerNameLowerCasedRecord, null) + +module.exports = { + wellknownHeaderNames, + headerNameLowerCasedRecord +} + + /***/ }), /***/ 8045: @@ -13479,6 +13605,7 @@ const { InvalidArgumentError } = __nccwpck_require__(8045) const { Blob } = __nccwpck_require__(4300) const nodeUtil = __nccwpck_require__(3837) const { stringify } = __nccwpck_require__(3477) +const { headerNameLowerCasedRecord } = __nccwpck_require__(4462) const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) @@ -13688,6 +13815,15 @@ function parseKeepAliveTimeout (val) { return m ? parseInt(m[1], 10) * 1000 : null } +/** + * Retrieves a header name and returns its lowercase value. + * @param {string | Buffer} value Header name + * @returns {string} + */ +function headerNameToString (value) { + return headerNameLowerCasedRecord[value] || value.toLowerCase() +} + function parseHeaders (headers, obj = {}) { // For H2 support if (!Array.isArray(headers)) return headers @@ -13959,6 +14095,7 @@ module.exports = { isIterable, isAsyncIterable, isDestroyed, + headerNameToString, parseRawHeaders, parseHeaders, parseKeepAliveTimeout, @@ -20606,14 +20743,18 @@ const { isBlobLike, toUSVString, ReadableStreamFrom } = __nccwpck_require__(3983 const assert = __nccwpck_require__(9491) const { isUint8Array } = __nccwpck_require__(9830) +let supportedHashes = [] + // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable /** @type {import('crypto')|undefined} */ let crypto try { crypto = __nccwpck_require__(6113) + const possibleRelevantHashes = ['sha256', 'sha384', 'sha512'] + supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash)) +/* c8 ignore next 3 */ } catch { - } function responseURL (response) { @@ -21141,66 +21282,56 @@ function bytesMatch (bytes, metadataList) { return true } - // 3. If parsedMetadata is the empty set, return true. + // 3. If response is not eligible for integrity validation, return false. + // TODO + + // 4. If parsedMetadata is the empty set, return true. if (parsedMetadata.length === 0) { return true } - // 4. Let metadata be the result of getting the strongest + // 5. Let metadata be the result of getting the strongest // metadata from parsedMetadata. - const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo)) - // get the strongest algorithm - const strongest = list[0].algo - // get all entries that use the strongest algorithm; ignore weaker - const metadata = list.filter((item) => item.algo === strongest) + const strongest = getStrongestMetadata(parsedMetadata) + const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest) - // 5. For each item in metadata: + // 6. For each item in metadata: for (const item of metadata) { // 1. Let algorithm be the alg component of item. const algorithm = item.algo // 2. Let expectedValue be the val component of item. - let expectedValue = item.hash + const expectedValue = item.hash // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e // "be liberal with padding". This is annoying, and it's not even in the spec. - if (expectedValue.endsWith('==')) { - expectedValue = expectedValue.slice(0, -2) - } - // 3. Let actualValue be the result of applying algorithm to bytes. let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') - if (actualValue.endsWith('==')) { - actualValue = actualValue.slice(0, -2) + if (actualValue[actualValue.length - 1] === '=') { + if (actualValue[actualValue.length - 2] === '=') { + actualValue = actualValue.slice(0, -2) + } else { + actualValue = actualValue.slice(0, -1) + } } // 4. If actualValue is a case-sensitive match for expectedValue, // return true. - if (actualValue === expectedValue) { - return true - } - - let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url') - - if (actualBase64URL.endsWith('==')) { - actualBase64URL = actualBase64URL.slice(0, -2) - } - - if (actualBase64URL === expectedValue) { + if (compareBase64Mixed(actualValue, expectedValue)) { return true } } - // 6. Return false. + // 7. Return false. return false } // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options // https://www.w3.org/TR/CSP2/#source-list-syntax // https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 -const parseHashWithOptions = /((?sha256|sha384|sha512)-(?[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i +const parseHashWithOptions = /(?sha256|sha384|sha512)-((?[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i /** * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata @@ -21214,8 +21345,6 @@ function parseMetadata (metadata) { // 2. Let empty be equal to true. let empty = true - const supportedHashes = crypto.getHashes() - // 3. For each token returned by splitting metadata on spaces: for (const token of metadata.split(' ')) { // 1. Set empty to false. @@ -21225,7 +21354,11 @@ function parseMetadata (metadata) { const parsedToken = parseHashWithOptions.exec(token) // 3. If token does not parse, continue to the next token. - if (parsedToken === null || parsedToken.groups === undefined) { + if ( + parsedToken === null || + parsedToken.groups === undefined || + parsedToken.groups.algo === undefined + ) { // Note: Chromium blocks the request at this point, but Firefox // gives a warning that an invalid integrity was given. The // correct behavior is to ignore these, and subsequently not @@ -21234,11 +21367,11 @@ function parseMetadata (metadata) { } // 4. Let algorithm be the hash-algo component of token. - const algorithm = parsedToken.groups.algo + const algorithm = parsedToken.groups.algo.toLowerCase() // 5. If algorithm is a hash function recognized by the user // agent, add the parsed token to result. - if (supportedHashes.includes(algorithm.toLowerCase())) { + if (supportedHashes.includes(algorithm)) { result.push(parsedToken.groups) } } @@ -21251,6 +21384,82 @@ function parseMetadata (metadata) { return result } +/** + * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList + */ +function getStrongestMetadata (metadataList) { + // Let algorithm be the algo component of the first item in metadataList. + // Can be sha256 + let algorithm = metadataList[0].algo + // If the algorithm is sha512, then it is the strongest + // and we can return immediately + if (algorithm[3] === '5') { + return algorithm + } + + for (let i = 1; i < metadataList.length; ++i) { + const metadata = metadataList[i] + // If the algorithm is sha512, then it is the strongest + // and we can break the loop immediately + if (metadata.algo[3] === '5') { + algorithm = 'sha512' + break + // If the algorithm is sha384, then a potential sha256 or sha384 is ignored + } else if (algorithm[3] === '3') { + continue + // algorithm is sha256, check if algorithm is sha384 and if so, set it as + // the strongest + } else if (metadata.algo[3] === '3') { + algorithm = 'sha384' + } + } + return algorithm +} + +function filterMetadataListByAlgorithm (metadataList, algorithm) { + if (metadataList.length === 1) { + return metadataList + } + + let pos = 0 + for (let i = 0; i < metadataList.length; ++i) { + if (metadataList[i].algo === algorithm) { + metadataList[pos++] = metadataList[i] + } + } + + metadataList.length = pos + + return metadataList +} + +/** + * Compares two base64 strings, allowing for base64url + * in the second string. + * +* @param {string} actualValue always base64 + * @param {string} expectedValue base64 or base64url + * @returns {boolean} + */ +function compareBase64Mixed (actualValue, expectedValue) { + if (actualValue.length !== expectedValue.length) { + return false + } + for (let i = 0; i < actualValue.length; ++i) { + if (actualValue[i] !== expectedValue[i]) { + if ( + (actualValue[i] === '+' && expectedValue[i] === '-') || + (actualValue[i] === '/' && expectedValue[i] === '_') + ) { + continue + } + return false + } + } + + return true +} + // https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { // TODO @@ -21666,7 +21875,8 @@ module.exports = { urlHasHttpsScheme, urlIsHttpHttpsScheme, readAllBytes, - normalizeMethodRecord + normalizeMethodRecord, + parseMetadata } @@ -23753,12 +23963,17 @@ function parseLocation (statusCode, headers) { // https://tools.ietf.org/html/rfc7231#section-6.4.4 function shouldRemoveHeader (header, removeContent, unknownOrigin) { - return ( - (header.length === 4 && header.toString().toLowerCase() === 'host') || - (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) || - (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') || - (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie') - ) + if (header.length === 4) { + return util.headerNameToString(header) === 'host' + } + if (removeContent && util.headerNameToString(header).startsWith('content-')) { + return true + } + if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) { + const name = util.headerNameToString(header) + return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization' + } + return false } // https://tools.ietf.org/html/rfc7231#section-6.4 @@ -29483,11 +29698,15 @@ class MermaidXYChart { } const xAxis = []; const seconds = []; - for (const [date, mean] of aggregates.entries()) { + for (const [date, value] of aggregates.entries()) { xAxis.unshift(`"${date}"`); - seconds.unshift(mean); + seconds.unshift(value); } const status = this.input.status ? ` for status=${this.input.status}` : ""; + // NOTE: When drawing the chart, if the minimum and maximum values of the y-axis are the same as the actual minimum and maximum values, + // the transition of the chart tends to be extreme. To avoid this, allow margins for the minimum and maximum values on the y-axis + const yAxisMin = Math.floor(Math.min(...seconds) * 0.8); + const yAxisMax = Math.floor(Math.max(...seconds) * 1.2); return ` \`\`\`mermaid --- @@ -29503,7 +29722,7 @@ config: xychart-beta title "${this.workflow.name} (${this.workflow.path}${status})" x-axis [${xAxis.join(",")}] - y-axis "Duration (${this.input.aggregate} in seconds)" + y-axis "Duration (${this.input.aggregate} in seconds)" ${yAxisMin} --> ${yAxisMax} bar [${seconds.join(",")}] \`\`\` `; diff --git a/src/MermaidXYChart.ts b/src/MermaidXYChart.ts index 9468781..76774d2 100644 --- a/src/MermaidXYChart.ts +++ b/src/MermaidXYChart.ts @@ -49,11 +49,15 @@ export class MermaidXYChart { } const xAxis: string[] = []; const seconds: number[] = []; - for (const [date, mean] of aggregates.entries()) { + for (const [date, value] of aggregates.entries()) { xAxis.unshift(`"${date}"`); - seconds.unshift(mean); + seconds.unshift(value); } const status = this.input.status ? ` for status=${this.input.status}` : ""; + // NOTE: When drawing the chart, if the minimum and maximum values of the y-axis are the same as the actual minimum and maximum values, + // the transition of the chart tends to be extreme. To avoid this, allow margins for the minimum and maximum values on the y-axis + const yAxisMin = Math.floor(Math.min(...seconds) * 0.8); + const yAxisMax = Math.floor(Math.max(...seconds) * 1.2); return ` \`\`\`mermaid --- @@ -69,7 +73,9 @@ config: xychart-beta title "${this.workflow.name} (${this.workflow.path}${status})" x-axis [${xAxis.join(",")}] - y-axis "Duration (${this.input.aggregate} in seconds)" + y-axis "Duration (${ + this.input.aggregate + } in seconds)" ${yAxisMin} --> ${yAxisMax} bar [${seconds.join(",")}] \`\`\` `; diff --git a/tests/GitHubIssueContent.test.ts b/tests/GitHubIssueContent.test.ts index 361add8..0018c32 100644 --- a/tests/GitHubIssueContent.test.ts +++ b/tests/GitHubIssueContent.test.ts @@ -84,7 +84,7 @@ config: xychart-beta title "ABC (abc.yml)" x-axis ["Feb 27"] - y-axis "Duration (min in seconds)" + y-axis "Duration (min in seconds)" 3 --> 4 bar [4] \`\`\` @@ -103,7 +103,7 @@ config: xychart-beta title "XYZ (xyz.yml)" x-axis ["Mar 27"] - y-axis "Duration (min in seconds)" + y-axis "Duration (min in seconds)" 1 --> 2 bar [2] \`\`\` `); diff --git a/tests/MermaidXYChart.test.ts b/tests/MermaidXYChart.test.ts index 2a08e16..c08b4db 100644 --- a/tests/MermaidXYChart.test.ts +++ b/tests/MermaidXYChart.test.ts @@ -107,7 +107,7 @@ config: xychart-beta title "ABC (abc.yml)" x-axis ["Feb 19","Feb 20","Feb 21","Feb 22","Feb 23","Feb 24","Feb 25","Feb 26","Feb 27"] - y-axis "Duration (average in seconds)" + y-axis "Duration (average in seconds)" 339 --> 1266 bar [424,942,971,1055,911,734,684,658,815] \`\`\` `); @@ -181,7 +181,7 @@ config: xychart-beta title "ABC (abc.yml)" x-axis ["Feb 19"] - y-axis "Duration (median in seconds)" + y-axis "Duration (median in seconds)" 344 --> 516 bar [430] \`\`\` `); @@ -238,7 +238,7 @@ config: xychart-beta title "ABC (abc.yml for status=success)" x-axis ["Feb 19"] - y-axis "Duration (max in seconds)" + y-axis "Duration (max in seconds)" 416 --> 625 bar [521] \`\`\` `);