From 400a6776e4bf48c02ff0293b41d0d9926db98b77 Mon Sep 17 00:00:00 2001 From: Zerebos Date: Sat, 4 Mar 2023 13:15:19 -0500 Subject: [PATCH] Implement semver version checking (#1560) --- renderer/src/modules/updater.js | 24 ++--------- renderer/src/structs/semver.js | 76 +++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 renderer/src/structs/semver.js diff --git a/renderer/src/modules/updater.js b/renderer/src/modules/updater.js index 1e65bfd3..b5e3b12b 100644 --- a/renderer/src/modules/updater.js +++ b/renderer/src/modules/updater.js @@ -20,6 +20,8 @@ import Modals from "../ui/modals"; import UpdaterPanel from "../ui/updater"; import DiscordModules from "./discordmodules"; +import {comparator as semverComparator, regex as semverRegex} from "../structs/semver"; + const React = DiscordModules.React; @@ -134,24 +136,6 @@ export class CoreUpdater { } } -const semverRegex = /^[0-9]+\.[0-9]+\.[0-9]+$/; - -/** - * This works on basic semantic versioning e.g. "1.0.0". - * - * @param {string} currentVersion - * @param {string} content - * @returns {boolean} whether there is an update - */ -function semverComparator(currentVersion, remoteVersion) { - currentVersion = currentVersion.split(".").map((e) => {return parseInt(e);}); - remoteVersion = remoteVersion.split(".").map((e) => {return parseInt(e);}); - - if (remoteVersion[0] > currentVersion[0]) return true; - else if (remoteVersion[0] == currentVersion[0] && remoteVersion[1] > currentVersion[1]) return true; - else if (remoteVersion[0] == currentVersion[0] && remoteVersion[1] == currentVersion[1] && remoteVersion[2] > currentVersion[2]) return true; - return false; -} class AddonUpdater { @@ -195,9 +179,9 @@ class AddonUpdater { if (this.pending.includes(filename)) return; const info = this.cache[path.basename(filename)]; if (!info) return; - let hasUpdate = info.update > currentVersion; + let hasUpdate = info.version > currentVersion; if (semverRegex.test(info.version) && semverRegex.test(currentVersion)) { - hasUpdate = semverComparator(currentVersion, info.version); + hasUpdate = semverComparator(currentVersion, info.version) > 0; } if (!hasUpdate) return; this.pending.push(filename); diff --git a/renderer/src/structs/semver.js b/renderer/src/structs/semver.js new file mode 100644 index 00000000..c9fc09f4 --- /dev/null +++ b/renderer/src/structs/semver.js @@ -0,0 +1,76 @@ +// "Official" regex from https://semver.org/ +export const regex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; + + +// Some of the code here adapted from https://github.com/npm/node-semver +const numeric = /^[0-9]+$/; +function compare(a, b) { + const anum = numeric.test(a); + const bnum = numeric.test(b); + + if (anum && bnum) { + a = +a; + b = +b; + } + + if (a === b) return 0; + else if (anum && !bnum) return -1; + else if (bnum && !anum) return 1; + else if (a < b) return -1; + return 1; +} + +function tokenize(pre) { + return pre.split(".").map((id) => { + if (!numeric.test(id)) return id; + const num = +id; // convert to number and check if valid + if (num >= 0 && num < Number.MAX_SAFE_INTEGER) return num; + return id; // Unsafe number => keep as string + }); +} + +function compareTokens(a, b) { + const atokens = tokenize(a); + const btokens = tokenize(b); + + // Compare token by token whether it's a string or number + for (let index = 0; ; index++) { + const x = atokens[index]; + const y = btokens[index]; + if (x === undefined && y === undefined) return 0; + else if (y === undefined) return 1; + else if (x === undefined) return -1; + else if (x === y) continue; + return compare(x, y); + } +} + +function preCompare(a, b) { + // Having a prerelease makes it a "lower" version + if (a.length && !b.length) return -1; + else if (!a.length && b.length) return 1; + else if (!a.length && !b.length) return 0; + return compareTokens(a, b); +} + +/** + * This works on semantic versioning e.g. "1.0.0". + * + * @param {string} currentVersion + * @param {string} remoteVersion + * @returns {number} 0 indicates equal, -1 indicates left hand greater, 1 indicates right hand greater + */ +export function comparator(currentVersion, remoteVersion) { + const current = regex.exec(currentVersion); + const remote = regex.exec(remoteVersion); + + // Raw match is at [0] so major starts at [1] + // This compares only the major, minor, and patch levels + const versionCompare = compare(remote[1], current[1]) || compare(remote[2], current[2]) || compare(remote[3], current[3]); + + // Also need to check prerelease and build info + const prereleaseCompare = preCompare(remote[4] ?? "", current[4] ?? ""); + const buildCompare = compareTokens(remote[5] ?? "", current[5] ?? ""); + + return versionCompare || prereleaseCompare || buildCompare; +} \ No newline at end of file