Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -1396,7 +1396,12 @@ An import reads the file as the pod is read, with the same mappers:
of those triples, which a prefix or a context can make far longer
than the text. They leave a wide margin over a deck of tens of
thousands of cards with progress, which the pod holds with no limit.
A release file the Studio opens is held to a release's limits
A Turtle file is refused the same way when it declares its base more
than 16 times or more than 4,096 prefixes. So is one whose base can
grow past 512 characters or has a `.`, `..` or empty segment in its
path. Its parse stops there, as it stops at the first triple past the
limits. A declaration quoted in a card's text doesn't count. A
release file the Studio opens is held to a release's limits
([deck-library.md](deck-library.md#from-a-link));
- a triple with a term Turtle can't write as it is is left out (below).

Expand Down
22 changes: 18 additions & 4 deletions docs/deck-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -961,10 +961,24 @@ flowchart LR
the limit is canceled as soon as it's past it, or before it's read
when its `Content-Length` says so. Parsing stops keeping triples at
the first one past either of the other limits, before any are built
into a dataset. A JSON-LD text stops being read there too: it's given
to the parser in parts, and the parser is stopped before the next. Each case refuses the release (`releaseTooLarge`),
however the release is read. The same limits apply to a creator's
catalog read for the release's series, to a release the Studio reads,
into a dataset. The text stops being read there too: it's given to
the parser in parts, and the parser gets no more after it. A
Turtle text is also refused at the part where it declares its base
(`@base`, `BASE`) for the 17th time, or where its base can grow past
512 characters (`writeTurtle`). It's refused there too when a base
it declares has a `.`, `..` or empty segment in its path, or when it
declares a prefix (`@prefix`, `PREFIX`) for the 4,097th time. The
length bound is the longest of the release's address and the
absolute bases it declares, with each relative one added on. The
parser walks the whole base for each relative IRI, base and prefix it
resolves, and a declaration makes no triple to count. The search
reads the text as the parser does, past strings, IRIs and comments,
so a declaration quoted in a card's text doesn't count. With the
bases and prefixes bounded and the parse stopped at the first limit,
the time to read or refuse a release grows with its size, not faster. Each case refuses
the release (`releaseTooLarge`), however the release is read. The
same limits apply to a creator's catalog read for the release's
series, to a release the Studio reads,
opens as a file, or checks against the library's shapes. The library's
index, which comes with the site and grows with the library, and the
user's own pod documents aren't limited. A deck file has limits of its
Expand Down
2 changes: 1 addition & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,7 @@ Dependency inversion gives every layer a seam for its fakes:

| Layer | Seam | Technique |
|---|---|---|
| Domain | none needed | Pure functions; inputs (including `now: Date`) passed as parameters. Plain assertions. What runs over a whole deck or log is timed at two sizes in its own process's CPU time: eight times the input must take under four times as long as an eighth of it done eight times (a linear pass gives about one, work growing with the input about eight). The growth tests (`growth` in `src/testing/growth.ts`, `growthAsync` for work that awaits, `growthAfterSetup` for work that changes what it works on, set up afresh before each run and not timed) time `queryCards` over 5,000 cards, `lapseIndex`, `chainIndexOf` and `statisticsOf` over years of answers, `instanceGraphOf` over an instance's cards and answers, a course's outline (`courseOutlineFromRecords`) over its chapters and steps, the learner's progress in it (`courseProgress`) over a chapter's steps, the release check's course rules (`courseProblems`), a draft's outline (`draftOutline`) over the chapters and steps, the cards the Studio's outline shows (`draftCardFinder`), the deck health's cards that say the same (`duplicateCardsOf`) over many cards alike, a JSON-LD copy (`jsonLdOf` in `solid`) over many values of one property, the shape check's report on a pod document and its check of a write (`createShaclShapeValidator` in `solid`, over an engine that answers at once) over a catalog's many links that aren't members and many dates it can't read, the notes on a release's making (`makingNotesOf`) over many in one language, a merge of drafts (`mergedDraft`) over many statements of one subject, the blank nodes a removal takes along (`withOnlyTheirBlankNodes` in `solid`) over many of one subject and many subjects naming one, a deck's cards added from a release, removed and brought to this app's format, one card's many distractors added and changed, a deck removed from a cards document another app wrote in, catalog entries and review states brought to this app's format, review states removed, a day's answers removed and many month summaries dropped from the digest, against a pod in memory (`importDeck`, `applyCardChanges`, `removeDeck`, `upgradeCards`, `upgradeDecks`, `upgradeReviewStates`, `applyReviewChanges`, `removeDay` and `updateDigest` in `solid`), a subject's many values set in one call (`setIriAll` and `setStringAll`, in the client modules' contract tests), and the Studio's graph layout and routes over a deck's cards. |
| Domain | none needed | Pure functions; inputs (including `now: Date`) passed as parameters. Plain assertions. What runs over a whole deck or log is timed at two sizes in its own process's CPU time: eight times the input must take under four times as long as an eighth of it done eight times (a linear pass gives about one, work growing with the input about eight). The growth tests (`growth` in `src/testing/growth.ts`, `growthAsync` for work that awaits, `growthAfterSetup` for work that changes what it works on, set up afresh before each run and not timed) time `queryCards` over 5,000 cards, `lapseIndex`, `chainIndexOf` and `statisticsOf` over years of answers, `instanceGraphOf` over an instance's cards and answers, a course's outline (`courseOutlineFromRecords`) over its chapters and steps, the learner's progress in it (`courseProgress`) over a chapter's steps, the release check's course rules (`courseProblems`), a draft's outline (`draftOutline`) over the chapters and steps, the cards the Studio's outline shows (`draftCardFinder`), the deck health's cards that say the same (`duplicateCardsOf`) over many cards alike, a JSON-LD copy (`jsonLdOf` in `solid`) over many values of one property, the search of a Turtle text for its base and prefix declarations (`writeTurtle` in `solid`), the shape check's report on a pod document and its check of a write (`createShaclShapeValidator` in `solid`, over an engine that answers at once) over a catalog's many links that aren't members and many dates it can't read, the notes on a release's making (`makingNotesOf`) over many in one language, a merge of drafts (`mergedDraft`) over many statements of one subject, the blank nodes a removal takes along (`withOnlyTheirBlankNodes` in `solid`) over many of one subject and many subjects naming one, a deck's cards added from a release, removed and brought to this app's format, one card's many distractors added and changed, a deck removed from a cards document another app wrote in, catalog entries and review states brought to this app's format, review states removed, a day's answers removed and many month summaries dropped from the digest, against a pod in memory (`importDeck`, `applyCardChanges`, `removeDeck`, `upgradeCards`, `upgradeDecks`, `upgradeReviewStates`, `applyReviewChanges`, `removeDay` and `updateDigest` in `solid`), a subject's many values set in one call (`setIriAll` and `setStringAll`, in the client modules' contract tests), and the Studio's graph layout and routes over a deck's cards. |
| Application | ports | Fake ports are injected (`vi.fn` per method): the fixtures and fake ports in [fakePorts.ts](../packages/application/src/testing/fakePorts.ts), and `NO_PORTS` ([noPorts.ts](../packages/application/src/testing/noPorts.ts)), which other packages' tests spread too, for the ports a test doesn't use. Each slice is tested in its own `<feature>.test.ts`, the stores the slices share in theirs. No module mocking. |
| UI | `UseCases` prop | Screens render with a fake `UseCases`: `makeUseCasesFake` ([useCasesFake.ts](../packages/ui/src/test/useCasesFake.ts)) makes every slice's fake ([test/fakes/](../packages/ui/src/test/fakes/)) in one object, each calling the others through it. A screen that takes a slice (the trial's) can be given that slice's fake alone. Assertions use testing-library queries. Query-dependent components get a fresh `QueryClient` (retries off). |
| Infrastructure mappers | none needed | Pure `Dataset`/`Subject` → domain functions, fed in-memory datasets built with `datasetFrom`/`buildThing` ([datasets.ts](../packages/solid/src/testing/datasets.ts)). |
Expand Down
53 changes: 38 additions & 15 deletions packages/solid-memo-client/src/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ import {
type RequestOptions,
type SourcedDataset,
} from "@solid-memo/solid/client/interface";
import { limitsOf, quadTally, refusalOf, textChunks } from "@solid-memo/solid/client/readLimits";
import {
limitsOf,
quadTally,
refusalOf,
type TextStream,
textChunks,
writeTurtle,
} from "@solid-memo/solid/client/readLimits";
import { MAX_PATCH_BYTES, patchBody } from "@solid-memo/solid/client/sparqlUpdate";
import { refuseUnwritable } from "@solid-memo/solid/writableTerms";
import {
Expand Down Expand Up @@ -151,35 +158,50 @@ function resourceOf(response: Response): Resource {
/**
* A collector of a text's quads, parsed as the document at `url` under
* `limits`: `add` keeps each, until one past them (quadTally), which
* drops what was kept and rejects once, with a ReadLimitError; the
* parser may go on to the end of the text, but nothing more is kept.
* drops what was kept and rejects once, with a ReadLimitError, as
* `refuse` does for a text past them otherwise (writeTurtle); nothing
* more is kept, and `refused` says to stop the parser.
*/
function collector(url: string, limits: ReadLimits | undefined, reject: (error: Error) => void) {
const quads: Quad[] = [];
const past = quadTally(limits);
let refused = false;
const refuse = (): void => {
refused = true;
quads.length = 0;
// The tally, and the bases, are past limits only under some.
reject(new ReadLimitError(url, limits!));
};
return {
quads,
refuse,
refused: () => refused,
add(quad: Quad): void {
if (refused) return;
if (!past(quad)) return void quads.push(quad);
refused = true;
quads.length = 0;
// The tally is past limits only under some.
reject(new ReadLimitError(url, limits!));
refuse();
},
};
}

/** The quads of a Turtle text, its relative IRIs resolved against `baseIRI`, under `limits` (collector). */
/**
* The quads of a Turtle text, its relative IRIs resolved against
* `baseIRI`, under `limits` (collector), as is a base past them
* (writeTurtle). The text is written to the parser in parts, so that it
* reads no further than the part it is in.
*/
function parseTurtle(text: string, baseIRI: string, limits?: ReadLimits): Promise<Quad[]> {
return new Promise((resolve, reject) => {
const { quads, add } = collector(baseIRI, limits, reject);
new Parser({ format: TURTLE, baseIRI }).parse(text, (error, quad) => {
if (error) reject(error);
else if (quad) add(quad);
else resolve(quads);
});
const { quads, add, refuse, refused } = collector(baseIRI, limits, reject);
const parser = new Parser({ format: TURTLE, baseIRI });
// n3's parser reads a stream as such an object, as its own stream parser hands it one.
const parse = (stream: TextStream) =>
parser.parse(stream as unknown as string, (error, quad) => {
if (error) reject(error);
else if (quad) add(quad);
else resolve(quads);
});
if (!writeTurtle(text, baseIRI, limits, parse, () => !refused())) refuse();
});
}

Expand Down Expand Up @@ -639,7 +661,8 @@ const MEDIA_TYPES = { turtle: TURTLE, jsonld: "application/ld+json" } as const;
* The quads of a Turtle or JSON-LD text, its relative IRIs resolved
* against `baseUrl`, parsed as a pod's documents are: in every graph the
* text names. Rejects with the parser's error on a text that does not
* parse, and with a ReadLimitError at the first quad past `limits`.
* parse, and with a ReadLimitError at the first quad past `limits`, or
* on a Turtle text at its first base past them (writeTurtle).
*/
export async function parseQuads(
text: string,
Expand Down
24 changes: 16 additions & 8 deletions packages/solid/src/client/linearDataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@

import {
getSolidDataset,
getSourceUrl,
getTurtleParser,
type Parser,
type SolidDataset,
} from "@inrupt/solid-client";
import type { Quad } from "@rdfjs/types";
import { ReadLimitError, type ReadLimits } from "./interface";
import { limitsOf, quadTally, refusalOf } from "./readLimits";
import { limitsOf, quadTally, refusalOf, type TextStream, writeTurtle } from "./readLimits";

type Graphs = SolidDataset["graphs"];
type Graph = Graphs["default"];
Expand Down Expand Up @@ -63,8 +64,9 @@ interface Collected {
/**
* solid-client's Turtle parser, whose quads go to `collected` instead of
* to solid-client; one past the limits of the read of `url` (quadTally)
* fails the parse instead, once, and drops what was kept. The parser
* goes on to the end of the text it was given, but keeps none of it.
* fails the parse instead, once, and drops what was kept, as does a
* base past them (writeTurtle). The text is written to the parser in
* parts, so that it reads no further than the part it is in.
*/
function collectingTurtleParser(collected: Collected, url: string): Parser {
const parser = getTurtleParser();
Expand All @@ -80,15 +82,21 @@ function collectingTurtleParser(collected: Collected, url: string): Parser {
const quads: Quad[] = [];
const past = quadTally(collected.limits);
collected.quads = quads;
parser.onQuad((quad) => {
if (collected.refused !== undefined) return;
if (!past(quad)) return quads.push(quad);
// The tally is past limits only under some.
const refuse = () => {
// The tally, and the bases, are past limits only under some.
collected.refused = new ReadLimitError(url, collected.limits!);
quads.length = 0;
for (const callback of failed) callback(collected.refused);
};
parser.onQuad((quad) => {
if (collected.refused !== undefined) return;
if (!past(quad)) return quads.push(quad);
refuse();
});
parser.parse(source, resourceInfo);
// solid-client hands the text to n3's parser as it is, which reads a stream as well.
const parse = (stream: TextStream) => parser.parse(stream as unknown as string, resourceInfo);
const goOn = () => collected.refused === undefined;
if (!writeTurtle(source, getSourceUrl(resourceInfo), collected.limits, parse, goOn)) refuse();
},
};
}
Expand Down
Loading
Loading