From bd78564ad7a799170e23a6501f90380cee010802 Mon Sep 17 00:00:00 2001 From: Daniel Jeffery Date: Tue, 24 Oct 2023 16:37:13 -0700 Subject: [PATCH] feat: YAML type validation --- README.md | 1 + server/package-lock.json | 118 ++++++++++++++++-- server/package.json | 8 +- server/src/server.common.ts | 130 +++++++++++++------ server/src/yaml-schema.ts | 12 -- server/src/yaml-utils.ts | 242 ++++++++++++++++++++++++++++++++++++ 6 files changed, 452 insertions(+), 59 deletions(-) delete mode 100644 server/src/yaml-schema.ts create mode 100644 server/src/yaml-utils.ts diff --git a/README.md b/README.md index 6720438..9848c90 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ See the [DEVELOPMENT](./docs/DEVELOPMENT.md) and [CONTRIBUTING](https://github.c ## Acknowledgments - CEL Textmate Grammar was taken from [vscode-cel](https://github.com/hmarr/vscode-cel) +- Range conversion from `yaml` to `vscode` from [actions/languageservices](https://github.com/actions/languageservices/blob/4280a967a8aa058dd3c8825349b90bc932d82283/workflow-parser/src/workflows/yaml-object-reader.ts#L220) ## License diff --git a/server/package-lock.json b/server/package-lock.json index 5ecaa0b..825fbea 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,9 +10,13 @@ "license": "Apache-2.0", "dependencies": { "@openfga/syntax-transformer": "^0.2.0-beta.4", + "ajv": "^8.12.0", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.11", - "yaml": "^2.3.2" + "yaml": "^2.3.3" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.8" }, "engines": { "node": "*" @@ -39,6 +43,27 @@ "antlr4": "^4.13.1" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.8.tgz", + "integrity": "sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==", + "dev": true + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/antlr4": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.1.tgz", @@ -80,6 +105,11 @@ "node": ">=0.4.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -112,6 +142,11 @@ "node": ">= 6" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -131,11 +166,35 @@ "node": ">= 0.6" } }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/tiny-async-pool": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-2.1.0.tgz", "integrity": "sha512-ltAHPh/9k0STRQqaoUX52NH4ZQYAJz24ZAEwf1Zm+HYg3l9OXTWeqWKyYsHu40wF/F0rxd2N2bk5sLvX2qlSvg==" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vscode-jsonrpc": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", @@ -175,9 +234,9 @@ "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, "node_modules/yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz", + "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==", "engines": { "node": ">= 14" } @@ -202,6 +261,23 @@ "antlr4": "^4.13.1" } }, + "@types/js-yaml": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.8.tgz", + "integrity": "sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==", + "dev": true + }, + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, "antlr4": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.1.tgz", @@ -234,6 +310,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -249,6 +330,11 @@ "mime-types": "^2.1.12" } }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -262,11 +348,29 @@ "mime-db": "1.52.0" } }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "tiny-async-pool": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-2.1.0.tgz", "integrity": "sha512-ltAHPh/9k0STRQqaoUX52NH4ZQYAJz24ZAEwf1Zm+HYg3l9OXTWeqWKyYsHu40wF/F0rxd2N2bk5sLvX2qlSvg==" }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, "vscode-jsonrpc": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", @@ -300,9 +404,9 @@ "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, "yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==" + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz", + "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==" } } } diff --git a/server/package.json b/server/package.json index 5a4a119..a60a4cc 100644 --- a/server/package.json +++ b/server/package.json @@ -13,9 +13,13 @@ }, "dependencies": { "@openfga/syntax-transformer": "^0.2.0-beta.4", + "ajv": "^8.12.0", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.11", - "yaml": "^2.3.2" + "yaml": "^2.3.3" }, - "scripts": {} + "scripts": {}, + "devDependencies": { + "@types/js-yaml": "^4.0.8" + } } diff --git a/server/src/server.common.ts b/server/src/server.common.ts index 293510e..32e8012 100644 --- a/server/src/server.common.ts +++ b/server/src/server.common.ts @@ -4,7 +4,6 @@ import { Diagnostic, DiagnosticSeverity, InitializeParams, - DidChangeConfigurationNotification, CompletionItem, TextDocumentPositionParams, TextDocumentSyncKind, @@ -30,8 +29,9 @@ import { validator, errors } from "@openfga/syntax-transformer"; import { defaultDocumentationMap } from "./documentation"; import { getDuplicationFix, getMissingDefinitionFix, getReservedTypeNameFix } from "./code-action"; import { LineCounter, parseDocument } from "yaml"; -import { rangeFromLinePos } from "./yaml-schema"; import { BlockMap, SourceToken } from "yaml/dist/parse/cst"; +import { YAMLSourceMap, openfgaYaml, rangeFromLinePos } from "./yaml-utils"; +import Ajv, { ErrorObject } from "ajv"; export function startServer(connection: _Connection) { @@ -94,10 +94,6 @@ export function startServer(connection: _Connection) { connection.onInitialized(() => { - if (hasConfigurationCapability) { - // Register for all configuration changes. - connection.client.register(DidChangeConfigurationNotification.type, undefined); - } if (hasWorkspaceFolderCapability) { connection.workspace.onDidChangeWorkspaceFolders(_event => { connection.console.log("Workspace folder change event received."); @@ -181,53 +177,111 @@ export function startServer(connection: _Connection) { connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); } - function validateYAML(textDocument: TextDocument): void { - connection.sendDiagnostics({ - uri: textDocument.uri, - diagnostics: [], - }); - + function validateYamlSyntaxAndModel(textDocument: TextDocument): Diagnostic[] { const diagnostics: Diagnostic[] = []; const lineCounter = new LineCounter(); - const doc = parseDocument(textDocument.getText(), { + const yamlDoc = parseDocument(textDocument.getText(), { lineCounter, keepSourceTokens: true, - uniqueKeys: false }); + const map = new YAMLSourceMap(); + map.doMap(yamlDoc.contents); + // Basic syntax errors - for (const err of doc.errors) { + for (const err of yamlDoc.errors) { diagnostics.push({ message: err.message, range: rangeFromLinePos(err.linePos) }); } - // Get location of model in CST - if (doc.has("model")) { - let position: {line: number, col: number}; - - // Get the model token and find its position - (doc.contents?.srcToken as BlockMap).items.forEach(i => { - if (i.key?.offset !== undefined && (i.key as SourceToken).source === "model") { - position = lineCounter.linePos(i.key?.offset); - } - }); + // If no diagnostics, continue parsing. + if (diagnostics.length === 0) { + + const validator = new Ajv(); + const validate = validator.compile(openfgaYaml); + + const valid = validate(yamlDoc.toJSON()); + if (!valid) { + validate.errors?.forEach((e: ErrorObject) => { + console.error(JSON.stringify(e as ErrorObject, null, 2)); + + let start = { line: 0, character: 0 }; + let end = { line: 0, character: 0 }; + let message; + + if (e.keyword === "oneOf") { + // We either have both model & model_file, or neither + message = "Must only define `model` or `model_file`."; + } else if (e.keyword === "additionalProperties") { + // If we've got invalid keys, mark them + let key = e.params["additionalProperty"]; + if (e.instancePath) { + const path = e.instancePath.substring(1).replace(/\//g, "."); + key = path.concat(".", key); + } + const range = map.nodes.get(key); + if (range) { + start = textDocument.positionAt(range?.[0]); + end = textDocument.positionAt(range?.[1]); + } + message = key + " is not a recognized key."; + } else { + // All other schema errors + const key = e.instancePath.substring(1).replace(/\//g, "."); + const range = map.nodes.get(key); + if (range) { + start = textDocument.positionAt(range?.[0]); + end = textDocument.positionAt(range?.[1]); + } + message = key + " " + e.message; + } + diagnostics.push({ message: message, range: { start, end } }); + }); + } + } - // Shift generated diagnostics by line of model, and indent of 2 - let dslDiagnostics = getDiagnosticsForDsl(doc.get("model") as string); - dslDiagnostics = dslDiagnostics.map(d => { - const r = d.range; - r.start.line += position.line; - r.start.character += 2; - r.end.line += position.line; - r.end.character += 2; - return d; - }); - diagnostics.push(...dslDiagnostics); + // Finally validate openfga model + if (diagnostics.length === 0) { + // Get location of model in CST + if (yamlDoc.has("model")) { + let position: { line: number, col: number }; + + // Get the model token and find its position + (yamlDoc.contents?.srcToken as BlockMap).items.forEach(i => { + if (i.key?.offset !== undefined && (i.key as SourceToken).source === "model") { + position = lineCounter.linePos(i.key?.offset); + } + }); + + // Shift generated diagnostics by line of model, and indent of 2 + let dslDiagnostics = getDiagnosticsForDsl(yamlDoc.get("model") as string); + dslDiagnostics = dslDiagnostics.map(d => { + const r = d.range; + r.start.line += position.line; + r.start.character += 2; + r.end.line += position.line; + r.end.character += 2; + return d; + }); + diagnostics.push(...dslDiagnostics); + } } - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + return diagnostics; } + function validateYAML(textDocument: TextDocument): void { + connection.sendDiagnostics({ + uri: textDocument.uri, + diagnostics: [], + }); + + const diagnostics: Diagnostic[] = []; + + diagnostics.push(...validateYamlSyntaxAndModel(textDocument)); + + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + } function validateTextDocument(textDocument: TextDocument): void { if (textDocument.languageId === "openfga") { @@ -356,4 +410,4 @@ export function startServer(connection: _Connection) { // Listen on the connection connection.listen(); -} \ No newline at end of file +} diff --git a/server/src/yaml-schema.ts b/server/src/yaml-schema.ts deleted file mode 100644 index 57ac311..0000000 --- a/server/src/yaml-schema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Range, Position } from "vscode-languageserver"; -import { LinePos } from "yaml/dist/errors"; - -export function rangeFromLinePos(linePos: [LinePos] | [LinePos, LinePos] | undefined): Range { - if (linePos === undefined) { - return { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }; - } - // TokenRange and linePos are both 1-based - const start: Position = { line: linePos[0].line - 1, character: linePos[0].col - 1 }; - const end: Position = linePos.length == 2 ? { line: linePos[1].line - 1, character: linePos[1].col - 1 } : start; - return { start, end }; -} \ No newline at end of file diff --git a/server/src/yaml-utils.ts b/server/src/yaml-utils.ts new file mode 100644 index 0000000..4fc0824 --- /dev/null +++ b/server/src/yaml-utils.ts @@ -0,0 +1,242 @@ +import { Range, Position } from "vscode-languageserver"; + +import { Range as TokenRange, isCollection, isDocument, isMap, isNode, isPair, isScalar, isSeq } from "yaml"; +import { LinePos } from "yaml/dist/errors"; + +export function rangeFromLinePos(linePos: [LinePos] | [LinePos, LinePos] | undefined): Range { + if (linePos === undefined) { + return { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }; + } + // TokenRange and linePos are both 1-based + const start: Position = { line: linePos[0].line - 1, character: linePos[0].col - 1 }; + const end: Position = linePos.length == 2 ? { line: linePos[1].line - 1, character: linePos[1].col - 1 } : start; + return { start, end }; +} + +export class YAMLSourceMap { + public nodes; + + constructor() { + this.nodes = new Map(); + } + + public doMap(node: any | null, path: string[] = []) { + + const localPath = [...path]; + + if (node === null) { + return; + } + + if (isMap(node)) { + for (const n of node.items) { + this.doMap(n, localPath); + } + return; + } + + if (isPair(node) && isScalar(node.key) && node.key.source) { + localPath.push(node.key.source); + this.doMap(node.key, localPath); + + if (isSeq(node.value)) { + for (const n in node.value.items) { + localPath.push(n); + this.doMap(node.value.items[n], localPath); + localPath.pop(); + } + } else if (isMap(node.value)) { + for (const n of node.value.items) { + this.doMap(n, localPath); + } + } + return; + } + + if (isScalar(node) && node.source && node.range) { + this.nodes.set(localPath.join("."), node.range); + return; + } + } +} + +export const openfgaYaml = { + type: "object", + oneOf: [ + { required: ["model_file"] }, + { required: ["model"] } + ], + properties: { + name: { + type: "string", + description: "The store name" + }, + model_file: { + type: "string", + description: "The Authorization Model" + }, + model: { + type: "string", + description: "The Authorization Model" + }, + tuples: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + user: { + type: "string", + description: "The user" + }, + relation: { + type: "string", + description: "The relation" + }, + object: { + type: "string", + description: "The object" + }, + condition: { + type: "object", + additionalProperties: false, + properties: { + name: { + type: "string", + }, + context: { + type: "object", + patternProperties: { + ".*": { + type: ["number", "string", "boolean", "array"] + } + } + } + } + } + } + } + }, + tests: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + name: { + type: "string", + description: "The test name" + }, + description: { + type: "string", + description: "The test description" + }, + tuples: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + user: { + type: "string", + description: "The user" + }, + relation: { + type: "string", + description: "The relation" + }, + object: { + type: "string", + description: "The object" + }, + context: { + type: "object", + patternProperties: { + ".*": { + type: ["number", "string", "boolean", "array"] + } + } + } + } + } + }, + check: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + user: { + type: "string", + description: "The user" + }, + object: { + type: "string", + description: "The object" + }, + assertions: { + type: "object", + patternProperties: { + ".*": { + type: "boolean" + } + } + }, + context: { + type: "object", + patternProperties: { + ".*": { + type: ["number", "string", "boolean", "array"] + } + } + } + } + } + }, + list_objects: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + user: { + type: "string", + description: "The user" + }, + relation: { + type: "string", + description: "The relation" + }, + type: { + type: "string", + description: "The object type" + }, + assertions: { + type: "object", + patternProperties: { + ".*": { + type: "array", + items: { + type: "string" + } + } + } + }, + context: { + type: "object", + patternProperties: { + ".*": { + type: ["number", "string", "boolean", "array"] + } + } + } + } + } + } + } + } + } + }, + required: ["tests"], + additionalProperties: false, +}; \ No newline at end of file