fix(profiles): follow-ups of the review of PR #436
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m8s
PR Check — Rust / rust (pull_request) Successful in 3m54s

- withoutFolders no longer keeps the first word of a folder that holds
  a blank (`C:\Users\Jean Dupont\...\profile_x.db` gave
  `Jean profile_x.db`): a path starts at a recognised prefix (drive,
  `/`, `~/`, `./`, UNC, `<profile folder>`) and runs to its last
  separator, blanks included, until a line break, a double quote, a
  backtick, `;`, `|`, a `,` or `:` before a blank, a closing parenthesis
  not followed by a separator, or a blank before another path. When
  nothing ends it, the text up to the last separator of the line is
  dropped rather than a folder let through. A quote before the path is
  not part of it, so `'C:\Users\Bob\x.db'` gives `'x.db'`. Relative runs
  with no recognised prefix keep the previous fallback. No error line
  contains a `/` nor a `\`, as before.
- ADR 0021 section 7 describes that rule and its limit.
- CHANGELOG.md, CHANGELOG.fr.md: no blank line between the two Fixed /
  Corrigé bullets.

Refs #395

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-10-13 19:57:29 -04:00
parent ac3a981f64
commit 86eff9b7a3
5 changed files with 115 additions and 8 deletions

View file

@ -10,7 +10,6 @@
### Corrigé ### Corrigé
- La page d'erreur du démarrage ne reste plus affichée quand le profil finit de s'ouvrir après le délai de 10 secondes : l'application s'affiche dès que le profil est ouvert (#395). - La page d'erreur du démarrage ne reste plus affichée quand le profil finit de s'ouvrir après le délai de 10 secondes : l'application s'affiche dès que le profil est ouvert (#395).
- Les installations faites depuis le paquet `.rpm` reçoivent désormais les mises à jour automatiques. Le manifeste de mise à jour ne listait que le `.deb`, qu'une installation `.rpm` téléchargeait puis refusait ; il liste maintenant chaque paquet Linux sous sa propre clé. Les installations `.rpm` existantes n'ont rien à réinstaller : elles recevront la prochaine version (vérifié sur le paquet v0.17.1 publié). Les instructions de téléchargement de la page de release ne mentionnent plus d'AppImage, qui n'est pas construite, et nomment désormais le `.rpm`. Le manifeste n'a plus d'entrée Linux générique : les AppImage des versions 0.4.3 à 0.4.5, qui ne savaient pas installer le `.deb` qu'elle désignait et en étaient cassées, affichent désormais une erreur à la vérification des mises à jour ; installez plutôt le paquet `.deb` ou `.rpm` (#320). - Les installations faites depuis le paquet `.rpm` reçoivent désormais les mises à jour automatiques. Le manifeste de mise à jour ne listait que le `.deb`, qu'une installation `.rpm` téléchargeait puis refusait ; il liste maintenant chaque paquet Linux sous sa propre clé. Les installations `.rpm` existantes n'ont rien à réinstaller : elles recevront la prochaine version (vérifié sur le paquet v0.17.1 publié). Les instructions de téléchargement de la page de release ne mentionnent plus d'AppImage, qui n'est pas construite, et nomment désormais le `.rpm`. Le manifeste n'a plus d'entrée Linux générique : les AppImage des versions 0.4.3 à 0.4.5, qui ne savaient pas installer le `.deb` qu'elle désignait et en étaient cassées, affichent désormais une erreur à la vérification des mises à jour ; installez plutôt le paquet `.deb` ou `.rpm` (#320).
### Sécurité ### Sécurité

View file

@ -10,7 +10,6 @@
### Fixed ### Fixed
- The startup error page no longer stays on screen when the profile finishes opening after the 10-second delay: the app now shows as soon as the profile is open (#395). - The startup error page no longer stays on screen when the profile finishes opening after the 10-second delay: the app now shows as soon as the profile is open (#395).
- Installs from the `.rpm` package now receive automatic updates. The update manifest only listed the `.deb`, which an `.rpm` install downloaded and then refused; it now lists each Linux package under its own key. Existing `.rpm` installs need no reinstall: they will receive the next version (checked on the published v0.17.1 package). The download instructions of the release page no longer mention an AppImage, which is not built, and now name the `.rpm`. The manifest no longer has a generic Linux entry: the AppImage builds of 0.4.3 to 0.4.5, which could not install the `.deb` it pointed to and were broken by it, now report an error when checking for updates instead; install the `.deb` or `.rpm` package (#320). - Installs from the `.rpm` package now receive automatic updates. The update manifest only listed the `.deb`, which an `.rpm` install downloaded and then refused; it now lists each Linux package under its own key. Existing `.rpm` installs need no reinstall: they will receive the next version (checked on the published v0.17.1 package). The download instructions of the release page no longer mention an AppImage, which is not built, and now name the `.rpm`. The manifest no longer has a generic Linux entry: the AppImage builds of 0.4.3 to 0.4.5, which could not install the `.deb` it pointed to and were broken by it, now report an error when checking for updates instead; install the `.deb` or `.rpm` package (#320).
### Security ### Security

View file

@ -58,7 +58,7 @@ Comme sqlx pour le profil par défaut. Une version qui échoue sur une clé étr
### 7. L'échec reste ouvert ### 7. L'échec reste ouvert
Un rattrapage interrompu est écrit au Journal et le profil s'ouvre comme avant #390. Les messages nomment des fichiers, jamais le dossier : il contient le nom d'utilisateur du système, et une ligne d'erreur du Journal peut être jointe à une rétroaction (Loi 25). Un rattrapage qui s'arrête après sa copie la nomme quand même : `catch_up_backup` (nom de fichier seul) est rempli dès que la copie existe, et la ligne d'erreur du Journal finit par « copy of the previous state kept as … » (#395). Côté TypeScript, `withoutFolders` ne garde que le dernier segment de tout chemin d'une ligne d'erreur, message de repli compris : aucune ligne d'erreur ne contient `/` ni `\`. Un rattrapage interrompu est écrit au Journal et le profil s'ouvre comme avant #390. Les messages nomment des fichiers, jamais le dossier : il contient le nom d'utilisateur du système, et une ligne d'erreur du Journal peut être jointe à une rétroaction (Loi 25). Un rattrapage qui s'arrête après sa copie la nomme quand même : `catch_up_backup` (nom de fichier seul) est rempli dès que la copie existe, et la ligne d'erreur du Journal finit par « copy of the previous state kept as … » (#395). Côté TypeScript, `withoutFolders` réduit chaque chemin d'une ligne d'erreur, message de repli compris, à son dernier segment (le nom de fichier). Un chemin part d'un début reconnu (lecteur `C:\`, `/`, `~/`, `./`, partage `\\`, ou le `<profile folder>` de `scrub`) et court jusqu'à son dernier séparateur, espaces compris, pour qu'un dossier comme `C:\Users\Jean Dupont\` ou `/home/marie claire/` ne laisse aucun mot derrière lui ; il s'arrête à un saut de ligne, un guillemet double, une apostrophe inverse, `;`, `|`, une virgule ou un deux-points suivis d'un blanc, une parenthèse fermante qui n'est pas suivie d'un séparateur (`Program Files (x86)\` continue), ou un blanc suivi d'un autre chemin. Une apostrophe placée juste avant le chemin reste autour du nom de fichier (`'C:\Users\Bob\x.db'` donne `'x.db'`). Quand rien ne l'arrête, la règle est prudente : elle retire le texte qui suit jusqu'au dernier séparateur de la ligne plutôt que de laisser passer un dossier. Limite : un chemin relatif sans début reconnu (`dossier/sous dossier/x.db`) n'a que ses séparateurs remplacés par `-` ; il ne part pas du dossier personnel, donc n'en contient pas le nom. Tout autre séparateur devient `-` : aucune ligne d'erreur ne contient `/` ni `\`.
### 8. Un seul dossier des bases ### 8. Un seul dossier des bases

View file

@ -708,4 +708,45 @@ describe("describePreparation (#390)", () => {
// A separator that is no path: kept readable, still no slash. // A separator that is no path: kept readable, still no slash.
expect(withoutFolders("pragma_failed: disk I/O error")).toBe("pragma_failed: disk I-O error"); expect(withoutFolders("pragma_failed: disk I/O error")).toBe("pragma_failed: disk I-O error");
}); });
// A folder holding a blank must not leave its first word behind (#436 review, S1).
it.each([
["C:\\Users\\Jean Dupont\\AppData\\Roaming\\com.simpl.resultat\\profile_x.db", "profile_x.db"],
["C:/Users/Jean Dupont/x.db", "x.db"],
["/home/marie claire/.config/x.db", "x.db"],
["~/Jean Dupont/x.db", "x.db"],
["\\\\server\\share\\Jean Dupont\\x.db", "x.db"],
["C:\\Program Files (x86)\\Simpl\\x.db", "x.db"],
['"C:\\Users\\Jean Dupont\\x.db" failed', '"x.db" failed'],
["(see /home/marie claire/x.db) and I/O", "(see x.db) and I-O"],
["path=/home/marie claire/x.db;next", "path=x.db;next"],
])("withoutFolders reduces %s to its last segment", (input, expected) => {
const cleaned = withoutFolders(input);
expect(cleaned).toBe(expected);
expect(cleaned).not.toMatch(/Jean|Dupont|marie|claire|Users|home|Program|server/);
});
it("withoutFolders ends a path at sentence punctuation and at the next path", () => {
expect(withoutFolders("unable to open /home/marie claire/x.db: disk I/O error")).toBe(
"unable to open x.db: disk I-O error"
);
expect(withoutFolders("read /home/marie claire/a.db, then C:\\Users\\Jean Dupont\\b.db")).toBe(
"read a.db, then b.db"
);
expect(withoutFolders("'C:\\a\\b.db' and 'C:\\Users\\Jean Dupont\\d.db'")).toBe("'b.db' and 'd.db'");
});
it.each([["\""], [";"], ["|"], ["`"], ["\n"]])("withoutFolders ends the folders at %j", (stop) => {
expect(withoutFolders(`/home/marie claire/x.db${stop} I/O`)).toBe(`x.db${stop} I-O`);
});
it("withoutFolders keeps a single quote around the file name, both sides", () => {
expect(withoutFolders("'C:\\Users\\Bob\\x.db'")).toBe("'x.db'");
expect(withoutFolders("cannot open '/home/marie claire/x.db' now")).toBe("cannot open 'x.db' now");
});
it("withoutFolders drops more text rather than a folder when nothing ends the path", () => {
// Conservative: everything up to the last separator of the line goes.
expect(withoutFolders("/home/marie claire/x.db then I/O")).toBe("O");
});
}); });

View file

@ -321,22 +321,90 @@ export interface ProfilePreparation {
} }
/** /**
* A run of non-blank characters holding a `/` or `\`, with the placeholder * Where a path starts: the placeholder Rust's `scrub` puts for the profile
* Rust's `scrub` puts for the profile folder in front of it. An apostrophe * folder, a drive (`C:\`), the home (`~/`), `./` or `../`, or a separator
* stays inside the run, so `C:\Users\L'Heureux\x.db` is one path. * (rooted path, UNC share). Only after the start of the text or one of
* BEFORE_START, so the `/` of `I/O` starts nothing.
*/
const PATH_START = /<profile folder>[\\/]|[A-Za-z]:[\\/]|~[\\/]|\.{1,2}[\\/]|[\\/]/y;
const BEFORE_START = /[\s'"`(=:,[]/;
/** What ends the folders of a path for sure: a line break, a double quote or a backtick, `;`, `|`. */
const PATH_STOP = /[\n\r"`;|]/;
/** The last segment of a path: the file name, kept. */
const LAST_SEGMENT = /[^\s"`()<>\\/]*/y;
/**
* Fallback for what the first pass leaves: a run of non-blank characters
* holding a `/` or `\` that no PATH_START opened (`dir/sub/x.db`, `I/O`).
*/ */
const PATH_RUN = /(?:<profile folder>)?[^\s"`()<>]*[\\/][^\s"`()<>]*/g; const PATH_RUN = /(?:<profile folder>)?[^\s"`()<>]*[\\/][^\s"`()<>]*/g;
/** A run that names a place on disk: rooted, home, relative to `.`, a drive, or two separators. */ /** A run that names a place on disk: rooted, home, relative to `.`, a drive, or two separators. */
const LOOKS_LIKE_PATH = /^(?:<profile folder>|[\\/~.]|[A-Za-z]:)|[\\/].*[\\/]/; const LOOKS_LIKE_PATH = /^(?:<profile folder>|[\\/~.]|[A-Za-z]:)|[\\/].*[\\/]/;
/** Length of the path start at `i`, 0 when no path starts there. */
function pathStartAt(text: string, i: number): number {
if (i > 0 && !BEFORE_START.test(text[i - 1])) return 0;
PATH_START.lastIndex = i;
return PATH_START.exec(text)?.[0].length ?? 0;
}
/** Whether a new path starts after the blank at `k` (quotes and an opening parenthesis skipped). */
function newPathAfter(text: string, k: number): boolean {
let j = k + 1;
while (j < text.length && /['"`(]/.test(text[j])) j++;
return pathStartAt(text, j) > 0;
}
/** Index of the last separator of the path whose start ends at `from` - 1. */
function lastSeparator(text: string, from: number): number {
let last = from - 1;
for (let k = from; k < text.length; k++) {
const c = text[k];
const next = text[k + 1] ?? "";
if (c === "\\" || c === "/") {
last = k;
continue;
}
if (PATH_STOP.test(c)) break;
// Sentence punctuation: Windows forbids `:` in a name, and no user name holds `,`.
if ((c === "," || c === ":") && (next === "" || /\s/.test(next))) break;
// `Program Files (x86)\…` goes on; `(see /x/y.db) and…` stops.
if (c === ")" && next !== "\\" && next !== "/") break;
if (/\s/.test(c) && newPathAfter(text, k)) break;
}
return last;
}
/** /**
* Keep only the last segment of every path in `text`: error lines can be * Keep only the last segment of every path in `text`: error lines can be
* attached to a feedback report, and a folder holds the OS user name * attached to a feedback report, and a folder holds the OS user name
* (Loi 25). Any other separator becomes `-` (SQLite's "disk I/O error" reads * (Loi 25). A folder may contain blanks (`C:\Users\Jean Dupont\…`,
* `/home/marie claire/…`): from its start, a path runs to its LAST separator
* before a line break, a double quote, a backtick, `;`, `|`, a `,` or `:`
* followed by a blank, a closing parenthesis, or a blank followed by another
* path. When nothing ends it, the rule is conservative: text after a path is
* dropped up to the last separator of the line (`/a/x.db then I/O` gives
* `O`), rather than letting a folder through. A quote right before the path is
* not part of it, so both quotes stay around the file name
* (`'C:\Users\Bob\x.db'` gives `'x.db'`). Any other separator becomes `-` (SQLite's "disk I/O error" reads
* "disk I-O error"), so an error line never contains a `/` nor a `\` (#395). * "disk I-O error"), so an error line never contains a `/` nor a `\` (#395).
*/ */
export function withoutFolders(text: string): string { export function withoutFolders(text: string): string {
return text.replace(PATH_RUN, (run) => let out = "";
let i = 0;
while (i < text.length) {
const start = pathStartAt(text, i);
if (start === 0) {
out += text[i];
i += 1;
continue;
}
const from = lastSeparator(text, i + start) + 1;
LAST_SEGMENT.lastIndex = from;
const segment = LAST_SEGMENT.exec(text)?.[0] ?? "";
out += segment;
i = from + segment.length;
}
return out.replace(PATH_RUN, (run) =>
LOOKS_LIKE_PATH.test(run) LOOKS_LIKE_PATH.test(run)
? run.split(/[\\/]/).filter(Boolean).pop() ?? "" ? run.split(/[\\/]/).filter(Boolean).pop() ?? ""
: run.replace(/[\\/]/g, "-") : run.replace(/[\\/]/g, "-")