Skip to content

Commit

Permalink
Ensure no input values are modified anywhere
Browse files Browse the repository at this point in the history
This commits adds a more reliable method of cloning SolidDatasets
and Files, rather than sneakily modifying the input value sometimes
and hoping nobody notices.
  • Loading branch information
Vinnl committed Oct 6, 2020
1 parent 9ada34a commit 1c9f1a1
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 109 deletions.
21 changes: 21 additions & 0 deletions src/acl/acl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
deleteAclFor,
createAcl,
internal_getContainerPath,
hasAcl,
} from "./acl";
import {
WithResourceInfo,
Expand All @@ -59,6 +60,7 @@ import {
AclDataset,
Access,
WithAccessibleAcl,
WithAcl,
} from "../interfaces";

function mockResponse(
Expand Down Expand Up @@ -406,6 +408,25 @@ describe("getContainerPath", () => {
});
});

describe("hasAcl", () => {
it("returns true if a Resource was fetched with its ACL Resources attached", () => {
const withAcl: WithAcl = {
internal_acl: {
resourceAcl: null,
fallbackAcl: null,
},
};

expect(hasAcl(withAcl)).toBe(true);
});

it("returns false if a Resource was fetched without its ACL Resources attached", () => {
const withoutAcl = {};

expect(hasAcl(withoutAcl)).toBe(false);
});
});

describe("getResourceAcl", () => {
it("returns the attached Resource ACL Dataset", () => {
const aclDataset: AclDataset = Object.assign(dataset(), {
Expand Down
24 changes: 16 additions & 8 deletions src/acl/acl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
getSourceUrl,
internal_defaultFetchOptions,
getResourceInfo,
internal_cloneResource,
} from "../resource/resource";
import { addIri } from "..";

Expand Down Expand Up @@ -136,6 +137,20 @@ export function internal_getContainerPath(resourcePath: string): string {
return containerPath;
}

/**
* Verify whether a given SolidDataset was fetched together with its Access Control List.
*
* Please note that the Web Access Control specification is not yet finalised, and hence, this
* function is still experimental and can change in a non-major release.
*
* @param dataset A [[SolidDataset]] that may have its ACLs attached.
* @returns True if `dataset` was fetched together with its ACLs.
*/
export function hasAcl<T extends object>(dataset: T): dataset is T & WithAcl {
const potentialAcl = dataset as T & WithAcl;
return typeof potentialAcl.internal_acl === "object";
}

/**
* ```{note} The Web Access Control specification is not yet finalised. As such, this
* function is still experimental and subject to change, even in a non-major release.
Expand Down Expand Up @@ -309,13 +324,6 @@ export function createAclFromFallbackAcl(
return initialisedResourceAcl;
}

/** @internal */
export function internal_isAclDataset(
dataset: SolidDataset
): dataset is AclDataset {
return typeof (dataset as AclDataset).internal_accessTo === "string";
}

/** @internal */
export function internal_getAclRules(aclDataset: AclDataset): AclRule[] {
const things = getThingAll(aclDataset);
Expand Down Expand Up @@ -641,7 +649,7 @@ export async function deleteAclFor<
);
}

const storedResource = Object.assign(resource, {
const storedResource = Object.assign(internal_cloneResource(resource), {
acl: {
resourceAcl: null,
},
Expand Down
8 changes: 4 additions & 4 deletions src/acl/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
WithFallbackAcl,
UrlString,
} from "../interfaces";
import { getSourceIri } from "../resource/resource";
import { getSourceIri, internal_cloneResource } from "../resource/resource";
import { createAcl, internal_getContainerPath } from "./acl";
import { mockContainerFrom } from "../resource/mock";

Expand All @@ -50,7 +50,7 @@ export function addMockResourceAclTo<T extends WithResourceInfo>(
const aclUrl =
resource.internal_resourceInfo.aclUrl ?? "https://your.pod/mock-acl.ttl";
const resourceWithAclUrl: typeof resource & WithAccessibleAcl = Object.assign(
resource,
internal_cloneResource(resource),
{
internal_resourceInfo: {
...resource.internal_resourceInfo,
Expand Down Expand Up @@ -96,7 +96,7 @@ export function addMockFallbackAclTo<T extends WithResourceInfo>(
const aclDataset = createAcl(mockContainer);

const resourceWithFallbackAcl: typeof resource &
WithFallbackAcl = Object.assign(resource, {
WithFallbackAcl = Object.assign(internal_cloneResource(resource), {
internal_acl: {
resourceAcl:
((resource as unknown) as WithAcl).internal_acl?.resourceAcl ?? null,
Expand All @@ -112,7 +112,7 @@ function setMockAclUrl<T extends WithResourceInfo>(
aclUrl: UrlString
): T & WithAccessibleAcl {
const resourceWithAclUrl: typeof resource & WithAccessibleAcl = Object.assign(
resource,
internal_cloneResource(resource),
{
internal_resourceInfo: {
...resource.internal_resourceInfo,
Expand Down
45 changes: 23 additions & 22 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export type WebId = UrlString;
* A SolidDataset represents all Quads from a single Resource.
*/
export type SolidDataset = DatasetCore;

/**
* A File is anything stored on a Pod in a format that solid-client does not have special affordances for, e.g. an image, or a plain JSON file.
*/
export type File = Blob;

/**
* A Resource is something that can be fetched from a Pod - either structured data in a [[SolidDataset]], or any other [[File]].
*/
export type Resource = SolidDataset | File;

/**
* A Thing represents all Quads with a given Subject URL and a given Named
* Graph, from a single Resource.
Expand Down Expand Up @@ -143,7 +154,7 @@ export type WithResourceInfo = {
/**
* @hidden Data structure to keep track of operations done by us; should not be read or manipulated by the developer.
*/
export type WithChangeLog = {
export type WithChangeLog = SolidDataset & {
internal_changeLog: {
additions: Quad[];
deletions: Quad[];
Expand All @@ -169,7 +180,9 @@ export type WithAcl = {
* Please note that the Web Access Control specification is not yet finalised, and hence, this
* function is still experimental and can change in a non-major release.
*/
export type WithResourceAcl<Resource extends WithAcl = WithAcl> = Resource & {
export type WithResourceAcl<
ResourceExt extends WithAcl = WithAcl
> = ResourceExt & {
internal_acl: {
resourceAcl: Exclude<WithAcl["internal_acl"]["resourceAcl"], null>;
};
Expand All @@ -181,7 +194,9 @@ export type WithResourceAcl<Resource extends WithAcl = WithAcl> = Resource & {
* Please note that the Web Access Control specification is not yet finalised, and hence, this
* function is still experimental and can change in a non-major release.
*/
export type WithFallbackAcl<Resource extends WithAcl = WithAcl> = Resource & {
export type WithFallbackAcl<
ResourceExt extends WithAcl = WithAcl
> = ResourceExt & {
internal_acl: {
fallbackAcl: Exclude<WithAcl["internal_acl"]["fallbackAcl"], null>;
};
Expand Down Expand Up @@ -221,29 +236,15 @@ export function hasChangelog<T extends SolidDataset>(
);
}

/**
* Verify whether a given SolidDataset was fetched together with its Access Control List.
*
* Please note that the Web Access Control specification is not yet finalised, and hence, this
* function is still experimental and can change in a non-major release.
*
* @param dataset A [[SolidDataset]] that may have its ACLs attached.
* @returns True if `dataset` was fetched together with its ACLs.
*/
export function hasAcl<T extends object>(dataset: T): dataset is T & WithAcl {
const potentialAcl = dataset as T & WithAcl;
return typeof potentialAcl.internal_acl === "object";
}

/**
* If this type applies to a Resource, its Access Control List, if it exists, is accessible to the currently authenticated user.
*
* Please note that the Web Access Control specification is not yet finalised, and hence, this
* function is still experimental and can change in a non-major release.
*/
export type WithAccessibleAcl<
Resource extends WithResourceInfo = WithResourceInfo
> = Resource & {
ResourceExt extends WithResourceInfo = WithResourceInfo
> = ResourceExt & {
internal_resourceInfo: {
aclUrl: Exclude<
WithResourceInfo["internal_resourceInfo"]["aclUrl"],
Expand All @@ -264,9 +265,9 @@ export type WithAccessibleAcl<
* @param dataset A [[SolidDataset]].
* @returns Whether the given `dataset` has a an ACL that is accessible to the current user.
*/
export function hasAccessibleAcl<Resource extends WithResourceInfo>(
dataset: Resource
): dataset is WithAccessibleAcl<Resource> {
export function hasAccessibleAcl<ResourceExt extends WithResourceInfo>(
dataset: ResourceExt
): dataset is WithAccessibleAcl<ResourceExt> {
return typeof dataset.internal_resourceInfo.aclUrl === "string";
}

Expand Down
3 changes: 2 additions & 1 deletion src/resource/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
Url,
UrlString,
SolidDataset,
File,
WithResourceInfo,
internal_toIriString,
} from "../interfaces";
Expand Down Expand Up @@ -107,7 +108,7 @@ export function mockFileFrom(
}>
): Unpromisify<ReturnType<typeof getFile>> {
const file = new Blob();
const fileWithResourceInfo: Blob & WithResourceInfo = Object.assign(file, {
const fileWithResourceInfo: File & WithResourceInfo = Object.assign(file, {
internal_resourceInfo: {
sourceIri: internal_toIriString(url),
isRawData: true,
Expand Down
8 changes: 2 additions & 6 deletions src/resource/nonRdfData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,9 +468,7 @@ describe("Non-RDF data deletion", () => {
});

describe("Write non-RDF data into a folder", () => {
const mockBlob = {
type: "binary",
} as Blob;
const mockBlob = new Blob(["mock blob data"], { type: "binary" });

type MockFetch = jest.Mock<
ReturnType<typeof window.fetch>,
Expand Down Expand Up @@ -691,9 +689,7 @@ describe("Write non-RDF data into a folder", () => {
});

describe("Write non-RDF data directly into a resource (potentially erasing previous value)", () => {
const mockBlob = {
type: "binary",
} as Blob;
const mockBlob = new Blob(["mock blob data"], { type: "binary" });

it("should default to the included fetcher if no other fetcher is available", async () => {
const fetcher = jest.requireMock("../fetcher") as {
Expand Down
22 changes: 12 additions & 10 deletions src/resource/nonRdfData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import { fetch } from "../fetcher";
import { Headers } from "cross-fetch";
import {
File,
UploadRequestInit,
WithResourceInfo,
WithAcl,
Expand All @@ -36,6 +37,7 @@ import {
getSourceIri,
getResourceInfo,
isRawData,
internal_cloneResource,
} from "./resource";

type GetFileOptions = {
Expand Down Expand Up @@ -69,7 +71,7 @@ function containsReserved(header: Headers): boolean {
export async function getFile(
input: Url | UrlString,
options: Partial<GetFileOptions> = defaultGetFileOptions
): Promise<Blob & WithResourceInfo> {
): Promise<File & WithResourceInfo> {
const config = {
...defaultGetFileOptions,
...options,
Expand All @@ -83,7 +85,7 @@ export async function getFile(
}
const resourceInfo = internal_parseResourceInfo(response);
const data = await response.blob();
const fileWithResourceInfo: Blob & WithResourceInfo = Object.assign(data, {
const fileWithResourceInfo: File & WithResourceInfo = Object.assign(data, {
internal_resourceInfo: resourceInfo,
});

Expand Down Expand Up @@ -117,7 +119,7 @@ export async function getFile(
export async function getFileWithAcl(
input: Url | UrlString,
options: Partial<GetFileOptions> = defaultGetFileOptions
): Promise<Blob & WithResourceInfo & WithAcl> {
): Promise<File & WithResourceInfo & WithAcl> {
const file = await getFile(input, options);
const acl = await internal_fetchAcl(file, options);
return Object.assign(file, { internal_acl: acl });
Expand Down Expand Up @@ -171,9 +173,9 @@ type SaveFileOptions = GetFileOptions & {
*/
export async function saveFileInContainer(
folderUrl: Url | UrlString,
file: Blob,
file: File,
options: Partial<SaveFileOptions> = defaultGetFileOptions
): Promise<(Blob & WithResourceInfo) | null> {
): Promise<(File & WithResourceInfo) | null> {
const folderUrlString = internal_toIriString(folderUrl);
const response = await writeFile(folderUrlString, file, "POST", options);

Expand All @@ -192,7 +194,7 @@ export async function saveFileInContainer(

const fileIri = new URL(locationHeader, new URL(folderUrlString).origin).href;

const blobClone = new Blob([file]);
const blobClone = internal_cloneResource(file);

let resourceInfo: WithResourceInfo;
try {
Expand Down Expand Up @@ -228,9 +230,9 @@ export async function saveFileInContainer(
*/
export async function overwriteFile(
fileUrl: Url | UrlString,
file: Blob,
file: File,
options: Partial<GetFileOptions> = defaultGetFileOptions
): Promise<Blob & WithResourceInfo> {
): Promise<File & WithResourceInfo> {
const fileUrlString = internal_toIriString(fileUrl);
const response = await writeFile(fileUrlString, file, "PUT", options);

Expand All @@ -240,7 +242,7 @@ export async function overwriteFile(
);
}

const blobClone = new Blob([file]);
const blobClone = internal_cloneResource(file);
const resourceInfo = internal_parseResourceInfo(response);
resourceInfo.sourceIri = fileUrlString;
resourceInfo.isRawData = true;
Expand All @@ -259,7 +261,7 @@ export async function overwriteFile(
*/
async function writeFile(
targetUrl: UrlString,
file: Blob,
file: File,
method: "PUT" | "POST",
options: Partial<SaveFileOptions>
): Promise<Response> {
Expand Down

0 comments on commit 1c9f1a1

Please sign in to comment.