@wpelevator/wp-release 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -134,7 +134,7 @@ wp-release verify wp-content/plugins/my-plugin --checksums my-plugin.1.2.0.check
134
134
  - Manifest paths outside the package folder (absolute or with `..`) are rejected.
135
135
  - `--zip <file|url>` checks the files of a ZIP instead of a directory. The ZIP can be a URL, such as the one that WordPress.org builds for a release, and its single root folder is found automatically (or set `--root-folder`). The ZIP is extracted to a temporary directory after the same safety checks as `deploy-svn --from-zip`.
136
136
  - The version in the manifest must match the `Version` header of the checked files, so that an older release with its own valid manifest can't pass as a newer one.
137
- - A manifest can't list itself. A copy of the manifest and of its `.sig` inside the checked files, at the root with the same names as the `--checksums` file or at the path of `--embedded`, is skipped when comparing files, but it must be identical to the `--checksums` file. See [Manifest inside the plugin](#manifest-inside-the-plugin).
137
+ - A manifest can't list itself. A copy of the manifest and of its `.sig` inside the checked files, at the root with the same names as the `--checksums` file or at the path of `--embedded`, is skipped when comparing files, but it must be identical to the `--checksums` file and its `.sig`. A path that the manifest lists is never skipped, and a file that isn't a matching copy is reported as not in the checksums. See [Manifest inside the plugin](#manifest-inside-the-plugin).
138
138
 
139
139
  A checksums manifest proves only that the files match the manifest. With `--public-key <key>`, `verify` first checks `<checksums>.sig` against the base64 Ed25519 public key and fails with exit code `2` before comparing any file when the signature is missing or invalid, so that the manifest itself can be trusted. See [Sign](#sign).
140
140
 
@@ -158,7 +158,7 @@ wp-release deploy-svn build --trunk --svn-tag 1.2.0
158
158
  --checksums build/checksums.json --public-key "$PUBLIC_KEY"
159
159
  ```
160
160
 
161
- - The manifest can have any name and location, for example `--output build/includes/signatures/checksums.json`, and missing folders are created. Tell `verify` where the shipped copy is with `--embedded <path>`, like `--embedded includes/signatures/checksums.json`. The copy and its `.sig` are then skipped when comparing files, must exist, and must be identical to the `--checksums` file and its signature. Without `--embedded`, `verify` looks for the manifest name at the root and only checks it when it's there.
161
+ - The manifest can have any name and location, for example `--output build/includes/signatures/checksums.json`, and missing folders are created. Tell `verify` where the shipped copy is with `--embedded <path>`, like `--embedded includes/signatures/checksums.json`. Both the `--checksums` file and its `.sig` must then exist, both copies must be shipped, and they're skipped when comparing files and must be identical to the originals. The manifest can't list the embedded path. Without `--embedded`, `verify` looks for the manifest name at the root, and skips a file only when it exists, the matching original exists and the manifest doesn't list it. Otherwise it's checked like any other file, so a listed file that has the name of the manifest can't be deleted unnoticed, and a shipped `.sig` without an original is reported as not in the checksums.
162
162
 
163
163
  The public key has to come from somewhere other than the plugin that it checks, because someone who can overwrite the plugin files can also replace a key and a signature inside them. A copy of the manifest also doesn't prevent a downgrade to an older release that was signed too, so the installing side has to enforce a minimum version.
164
164
 
@@ -99,33 +99,20 @@ function getEmbeddedPath(checksumsPath, embedded) {
99
99
  (isAbsolute(embedded) ||
100
100
  embedded.includes('\\') ||
101
101
  path.startsWith('../') ||
102
- '.' === path)) {
102
+ ['.', '..'].includes(path))) {
103
103
  throw new ValidationError(`--embedded must be a path inside the plugin with forward slashes, got "${embedded}".`);
104
104
  }
105
105
  return embedded || path.endsWith('.json') ? path : null;
106
106
  }
107
107
  /**
108
- * Compare the copies of the manifest and its signature inside the checked
109
- * directory with the given ones. A release that ships the manifest and its
110
- * signature with the plugin files must ship the same bytes that were
111
- * signed, otherwise a verifier on the site would check something else. The
112
- * copies are not part of the manifest, since a manifest can't list itself,
113
- * so they're skipped when comparing the files.
114
- *
115
- * Without an explicit path, the copies are expected at the root with the
116
- * name of the manifest, and are checked only when they exist. With an
117
- * explicit path they must exist.
108
+ * Get the manifest and signature pairs to look for inside the checked files.
118
109
  *
119
- * @param input Directory, manifest and embedded path.
120
- * @param input.dir Directory to check.
121
- * @param input.checksumsPath Manifest path.
122
- * @param input.embedded Explicit path of the embedded manifest.
123
- * @return The copies that were found to differ or be missing, and their paths.
110
+ * @param checksumsPath Manifest path.
111
+ * @param path Relative path of the embedded manifest, if any.
112
+ * @return Pairs of the originals and their copies.
124
113
  */
125
- async function checkEmbeddedCopies(input) {
126
- const { dir, checksumsPath, embedded } = input;
127
- const path = getEmbeddedPath(checksumsPath, embedded);
128
- const pairs = path
114
+ function getCopyPairs(checksumsPath, path) {
115
+ return path
129
116
  ? [
130
117
  { original: checksumsPath, copy: path },
131
118
  {
@@ -134,28 +121,82 @@ async function checkEmbeddedCopies(input) {
134
121
  },
135
122
  ]
136
123
  : [];
137
- const results = await Promise.all(pairs.map(async ({ original, copy }) => {
138
- const file = join(dir, copy);
139
- const hasCopy = existsSync(file);
140
- const hasOriginal = existsSync(original);
141
- const isSame = !hasCopy ||
142
- !hasOriginal ||
143
- resolve(original) === resolve(file) ||
144
- (await readFile(original)).equals(await readFile(file));
145
- return {
146
- copy,
147
- missing: Boolean(embedded) && hasOriginal && !hasCopy,
148
- differing: !isSame,
149
- };
150
- }));
124
+ }
125
+ /**
126
+ * Check that an explicit --embedded path can be verified: the manifest can't
127
+ * list its own copies, and the originals to compare with must exist.
128
+ *
129
+ * @param pairs Pairs of the originals and their copies.
130
+ * @param manifest Checksums manifest.
131
+ * @throws {ValidationError} If a copy is listed or an original is missing.
132
+ */
133
+ function checkExplicitPairs(pairs, manifest) {
134
+ pairs.forEach(({ original, copy }) => {
135
+ if (Object.hasOwn(manifest.files, copy)) {
136
+ throw new ValidationError(`The checksums list ${copy}, but a manifest can't list its own embedded copy.`);
137
+ }
138
+ if (!existsSync(original)) {
139
+ throw new ValidationError(`--embedded needs ${original} to compare ${copy} with, but it doesn't exist.`);
140
+ }
141
+ });
142
+ }
143
+ /**
144
+ * Check that two paths are the same file or have the same bytes.
145
+ *
146
+ * @param original Path of the original.
147
+ * @param file Path of the copy.
148
+ * @return Whether the contents are identical.
149
+ */
150
+ async function hasSameContents(original, file) {
151
+ return (resolve(original) === resolve(file) ||
152
+ (await readFile(original)).equals(await readFile(file)));
153
+ }
154
+ /**
155
+ * Compare the copies of the manifest and its signature inside the checked
156
+ * directory with the given ones. A release that ships the manifest and its
157
+ * signature with the plugin files must ship the same bytes that were
158
+ * signed, otherwise a verifier on the site would check something else. The
159
+ * copies are not part of the manifest, since a manifest can't list itself,
160
+ * so they're skipped when comparing the files.
161
+ *
162
+ * With an explicit path both originals and both copies must exist. Without
163
+ * one, the copies are expected at the root with the name of the manifest,
164
+ * and a file is treated as a copy only when it exists, the matching
165
+ * original exists and the manifest doesn't list it. Any other file is
166
+ * checked like the rest of the files.
167
+ *
168
+ * @param input Directory, manifest and embedded path.
169
+ * @param input.dir Directory to check.
170
+ * @param input.manifest Checksums manifest.
171
+ * @param input.checksumsPath Manifest path.
172
+ * @param input.embedded Explicit path of the embedded manifest.
173
+ * @return The copies that were found to differ or be missing, and the paths of the existing copies.
174
+ * @throws {ValidationError} If an explicit path can't be verified.
175
+ */
176
+ async function checkEmbeddedCopies(input) {
177
+ const { dir, manifest, checksumsPath, embedded } = input;
178
+ const pairs = getCopyPairs(checksumsPath, getEmbeddedPath(checksumsPath, embedded));
179
+ if (embedded) {
180
+ checkExplicitPairs(pairs, manifest);
181
+ }
182
+ const found = pairs.filter(({ original, copy }) => existsSync(join(dir, copy)) &&
183
+ (embedded ||
184
+ (existsSync(original) &&
185
+ !Object.hasOwn(manifest.files, copy))));
186
+ const results = await Promise.all(found.map(async ({ original, copy }) => ({
187
+ copy,
188
+ isSame: await hasSameContents(original, join(dir, copy)),
189
+ })));
151
190
  return {
152
- paths: pairs.map(({ copy }) => copy),
191
+ paths: found.map(({ copy }) => copy),
153
192
  differing: results
154
- .filter((result) => result.differing)
155
- .map((result) => result.copy),
156
- missing: results
157
- .filter((result) => result.missing)
158
- .map((result) => result.copy),
193
+ .filter(({ isSame }) => !isSame)
194
+ .map(({ copy }) => copy),
195
+ missing: embedded
196
+ ? pairs
197
+ .filter(({ copy }) => !existsSync(join(dir, copy)))
198
+ .map(({ copy }) => copy)
199
+ : [],
159
200
  };
160
201
  }
161
202
  /**
@@ -193,6 +234,7 @@ async function verifyTarget(input) {
193
234
  const { dir } = target;
194
235
  const copies = await checkEmbeddedCopies({
195
236
  dir,
237
+ manifest,
196
238
  checksumsPath,
197
239
  embedded,
198
240
  });
@@ -248,11 +290,13 @@ Options:
248
290
  --root-folder <name> Folder in the ZIP (default: the one folder that
249
291
  every entry is in)
250
292
  --embedded <path> Where the manifest and its .sig are shipped inside the
251
- files, like includes/checksums.json (default: the
252
- name of the --checksums file at the root, when it
253
- exists). They're skipped when comparing files and
254
- must be identical to the --checksums file and its
255
- .sig, and with this option they must exist
293
+ files, like includes/checksums.json. Both the
294
+ --checksums file and its .sig must exist, both
295
+ copies must be shipped, and they must be identical
296
+ to them. Without this option, a file with the name
297
+ of the --checksums file at the root (and its .sig)
298
+ is a copy only when it exists, the matching original
299
+ exists and the manifest doesn't list it
256
300
  --public-key <key> Base64 Ed25519 public key. Checks <checksums>.sig, as
257
301
  written by "wp-release sign", before comparing any
258
302
  file, and fails when it's missing or invalid
@@ -270,7 +314,9 @@ ignore file inside the checked directory can't hide added files. Use
270
314
 
271
315
  A manifest can't list itself. A copy of the manifest and of its .sig inside
272
316
  the checked files is therefore skipped, but it must be identical to the
273
- --checksums file. See --embedded for where it's looked for.`,
317
+ --checksums file. A path that the manifest lists is never skipped, and a
318
+ file that isn't a matching copy is reported as extra. See --embedded for
319
+ where the copies are looked for.`,
274
320
  options: {
275
321
  checksums: { type: 'string' },
276
322
  zip: { type: 'string' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpelevator/wp-release",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Package, checksum, sign, verify and version-check WordPress plugins and themes, and deploy plugins to WordPress.org SVN",
5
5
  "license": "MIT",
6
6
  "repository": {