Document opslaan overschrijft externe wijzigingen zonder conflictcontrole #1683

Closed
opened 2026-08-21 12:02:17 +00:00 by brenno · 2 comments
Owner

Probleem

Na het openen bewaart de documentstate geen bestandshash, mtime of andere versie-identiteit. Opslaan schrijft de in-memory bron daarom onvoorwaardelijk atomisch over het pad heen, ook als een andere editor, synchronisatietool of git-operatie het bestand sinds openen heeft gewijzigd.

Reproductie

  1. Open een document in OciDeck.
  2. Wijzig en sla hetzelfde bestand extern op.
  3. Wijzig iets in OciDeck en kies Opslaan.
  4. De externe wijziging verdwijnt zonder waarschuwing.

Verwacht

Voor overschrijven wordt gecontroleerd of het bestand nog overeenkomt met de ingelezen/savepointversie; bij conflict krijgt de gebruiker vergelijken, herladen of bewust overschrijven.

Technische aanwijzing

DocumentState bewaart savedSource maar geen schijfversie. saveDocument schrijft direct via writeStringAtomic en saveDocumentWithDestination doet geen preflight-read/hashvergelijking.

Gevonden bij audit van commit d439638c6b.

## Probleem Na het openen bewaart de documentstate geen bestandshash, mtime of andere versie-identiteit. Opslaan schrijft de in-memory bron daarom onvoorwaardelijk atomisch over het pad heen, ook als een andere editor, synchronisatietool of git-operatie het bestand sinds openen heeft gewijzigd. ## Reproductie 1. Open een document in OciDeck. 2. Wijzig en sla hetzelfde bestand extern op. 3. Wijzig iets in OciDeck en kies Opslaan. 4. De externe wijziging verdwijnt zonder waarschuwing. ## Verwacht Voor overschrijven wordt gecontroleerd of het bestand nog overeenkomt met de ingelezen/savepointversie; bij conflict krijgt de gebruiker vergelijken, herladen of bewust overschrijven. ## Technische aanwijzing DocumentState bewaart savedSource maar geen schijfversie. saveDocument schrijft direct via writeStringAtomic en saveDocumentWithDestination doet geen preflight-read/hashvergelijking. Gevonden bij audit van commit d439638c6bd1b519d68d87680cb022b7d5eddc85.
Author
Owner

Triage: accepted

Bevestigd tegen main (e93ef205c). DocumentState (lib/state/document_provider.dart) bewaart savedSource maar geen enkele identiteit van wat er op schijf staat. saveDocument (lib/services/file/file_service_open.dart) schrijft rechtstreeks via writeStringAtomic, en saveDocumentWithDestination (lib/widgets/shell/document_save_actions.dart) doet geen voorafgaande lezing.

Het patroon staat al in de repo. Grafiekdata doet precies deze controle: file_service_open.dart leest wat er op schijf staat, vergelijkt met de basislijn van bij het openen, en weigert mét waarschuwing bij een verschil. Wat daar geldt voor een data/*.json geldt zeker voor het document zelf.

Oplossingsrichting

  1. Vingerafdruk onthouden bij openen en bij elke geslaagde opslag. Grootte + mtime is genoeg om verschil te zien; een SHA-256 over de bytes is genoeg om het te weten, en documenten zijn klein naast de grens van 32 MiB — neem de hash.
  2. Voorafgaande lezing vóór het schrijven. Komt de vingerafdruk niet overeen, dan niet schrijven maar de gebruiker drie wegen geven: vergelijken, herladen (jouw wijzigingen weg), overschrijven (die van de ander weg). Nooit stil één daarvan kiezen.
  3. Bestand weg = geen conflict. Iemand heeft het verwijderd; schrijven is dan precies wat de gebruiker vroeg.
  4. Bouw het op de opslaglaag, niet in het documentscherm — de deck-kant heeft hetzelfde gat (_writeProject doet evenmin een controle) en hoort straks dezelfde poort te gebruiken. Maak daarvoor een eigen issue; dit issue blijft over het document.

Regressietest (verplicht)

  • Servicetest: openen, bestand extern wijzigen, opslaan → geweigerd, bestand op schijf ongewijzigd, en de aanroeper krijgt een conflict-uitkomst terug (geen false die als schrijffout leest).
  • Widgettest per uitweg: herladen laadt de externe tekst, overschrijven schrijft de eigen tekst, annuleren doet niets.
  • Ook de weg zónder conflict blijft groen: twee opslagen achter elkaar zonder externe wijziging mogen niet gaan klagen (de vingerafdruk moet ná elke schrijfactie bijgewerkt worden — daar zit de valkuil).

Kosten

Grootste van deze reeks. Opslaglaag + state + dialoog, en de dialoog kost ongeveer vijf nieuwe l10n.d('…') ⇒ 31 vertalingen elk (make add-l10n). Reken op een dag.

Prioriteit

Hoog: het werk van een ánder verdwijnt, en per definitie ziet niemand het gebeuren.

## Triage: accepted **Bevestigd tegen `main` (e93ef205c).** `DocumentState` (`lib/state/document_provider.dart`) bewaart `savedSource` maar geen enkele identiteit van wat er op schijf staat. `saveDocument` (`lib/services/file/file_service_open.dart`) schrijft rechtstreeks via `writeStringAtomic`, en `saveDocumentWithDestination` (`lib/widgets/shell/document_save_actions.dart`) doet geen voorafgaande lezing. **Het patroon staat al in de repo.** Grafiekdata doet precies deze controle: `file_service_open.dart` leest wat er op schijf staat, vergelijkt met de basislijn van bij het openen, en weigert mét waarschuwing bij een verschil. Wat daar geldt voor een `data/*.json` geldt zeker voor het document zelf. ## Oplossingsrichting 1. **Vingerafdruk onthouden** bij openen en bij elke geslaagde opslag. Grootte + mtime is genoeg om verschil te *zien*; een SHA-256 over de bytes is genoeg om het te *weten*, en documenten zijn klein naast de grens van 32 MiB — neem de hash. 2. **Voorafgaande lezing vóór het schrijven.** Komt de vingerafdruk niet overeen, dan niet schrijven maar de gebruiker drie wegen geven: vergelijken, herladen (jouw wijzigingen weg), overschrijven (die van de ander weg). Nooit stil één daarvan kiezen. 3. **Bestand weg = geen conflict.** Iemand heeft het verwijderd; schrijven is dan precies wat de gebruiker vroeg. 4. Bouw het op de opslaglaag, niet in het documentscherm — de deck-kant heeft hetzelfde gat (`_writeProject` doet evenmin een controle) en hoort straks dezelfde poort te gebruiken. **Maak daarvoor een eigen issue**; dit issue blijft over het document. ## Regressietest (verplicht) - Servicetest: openen, bestand extern wijzigen, opslaan → geweigerd, bestand op schijf ongewijzigd, en de aanroeper krijgt een conflict-uitkomst terug (geen `false` die als schrijffout leest). - Widgettest per uitweg: herladen laadt de externe tekst, overschrijven schrijft de eigen tekst, annuleren doet niets. - Ook de weg zónder conflict blijft groen: twee opslagen achter elkaar zonder externe wijziging mogen niet gaan klagen (de vingerafdruk moet ná elke schrijfactie bijgewerkt worden — daar zit de valkuil). ## Kosten Grootste van deze reeks. Opslaglaag + state + dialoog, en de dialoog kost ongeveer vijf nieuwe `l10n.d('…')` ⇒ 31 vertalingen elk (`make add-l10n`). Reken op een dag. ## Prioriteit Hoog: het werk van een ánder verdwijnt, en per definitie ziet niemand het gebeuren.
Author
Owner

De presentatiekant staat nu apart: #1699. Die is scherper dan hier geschetst — Deck.fileHash bestaat al (gezet bij openen én na elke schrijfactie), dus daar ontbreekt alleen de vergelijking vóór het schrijven. De dialoog en de poort horen gedeeld te worden met dit issue.

De presentatiekant staat nu apart: #1699. Die is scherper dan hier geschetst — `Deck.fileHash` bestaat al (gezet bij openen én na elke schrijfactie), dus daar ontbreekt alleen de vergelijking vóór het schrijven. De dialoog en de poort horen gedeeld te worden met dit issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#1683
No description provided.