├── .gitattributes
├── .gitignore
├── README.md
├── css
├── block.css
├── grid.css
├── index.css
└── inode.css
├── fs
├── Badinode
├── Good
├── Goodlarge
├── Goodlink
├── Goodrefcnt
├── Goodrm
├── Imrkfree
├── Imrkused
├── Mrkfree
├── Mrkused
└── Repair
├── index.html
├── js
├── block.js
├── config.js
├── filetree.js
├── grid.js
├── image.js
├── index.js
├── inode.js
└── text.min.js
├── screenrecord.gif
└── screenshot.png
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .idea
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xv6 File System Visualizer
2 |
3 | [This](https://shawnzhong.github.io/xv6-file-system-visualizer/) is an online visualizer for xv6 file system image. The source code is published at [GitHub](https://github.com/ShawnZhong/xv6-file-system-visualizer)
4 |
5 | ## Screenshot
6 |
7 | # 
8 |
9 | ## Screen Record
10 |
11 |
12 |
13 | ## Features
14 |
15 | - See the overall layout of an xv6 filesystem image
16 |
17 | - View the metadata stored in inodes
18 |
19 | - Trace the relationship between files/directories, inodes, and blocks
20 |
21 | - Check the file/directory path for inodes
22 |
23 | - Basic inconsistency checking:
24 |
25 | - Invalid inode type.
26 |
27 | - Inode marked use but not found in a directory.
28 |
29 | - Inode referred to in directory but marked free.
30 |
31 | - Block used by inode but marked free in bitmap.
32 |
33 | - Bitmap marks block in use but it is not in use.
34 |
--------------------------------------------------------------------------------
/css/block.css:
--------------------------------------------------------------------------------
1 | #block-container > .super-block {
2 | background-color: darkorange;
3 | }
4 |
5 | #block-container > .unused-block {
6 | background-color: gray;
7 | }
8 |
9 | #block-container > .inode-block {
10 | background-color: #4caf50;
11 | }
12 |
13 | #block-container > .bitmap-block {
14 | background-color: #f061ff;
15 | }
16 |
17 | #block-container > .data-block {
18 | background-color: cornflowerblue;
19 | }
20 |
--------------------------------------------------------------------------------
/css/grid.css:
--------------------------------------------------------------------------------
1 | .grid-container {
2 | display: flex;
3 | flex-wrap: wrap;
4 | text-align: center;
5 | color: white;
6 | }
7 |
8 | .grid-container.not-selected > div {
9 | color: lightgray;
10 | }
11 |
12 | .grid-container > div {
13 | width: 1.5rem;
14 | line-height: 1;
15 | padding: 0.25rem 0;
16 | border: solid 1px white;
17 | }
18 |
19 | .grid-container > div.hovered {
20 | box-shadow: 0 0 3px black;
21 | font-weight: bold;
22 | color: black;
23 | border-color: black;
24 | z-index: 0;
25 | }
26 |
27 | .grid-container > div.selected {
28 | font-weight: bold;
29 | color: black;
30 | box-shadow: 0 0 5px black;
31 | z-index: 0;
32 | }
33 |
34 | .grid-container > div.related {
35 | font-weight: bold;
36 | color: black;
37 | }
38 |
39 |
40 | .grid-container > div.error {
41 | background-color: #f44336;
42 | }
--------------------------------------------------------------------------------
/css/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | }
4 |
5 | pre {
6 | margin: 0;
7 | }
8 |
9 | #image-selector * {
10 | display: inline;
11 | }
12 |
13 | #image-list > div {
14 | margin-left: 0.2em;
15 | }
16 |
17 | #detail-content > div > pre {
18 | white-space: pre-wrap;
19 | tab-size: 6;
20 | }
21 |
22 | #detail-content > div > pre.text {
23 | word-break: break-all;
24 | }
25 |
26 |
27 | #file-tree-content > pre.related {
28 | color: black !important;
29 | font-weight: bold;
30 | text-decoration: underline;
31 | }
32 |
33 | #file-tree-content.not-selected > pre {
34 | color: gray;
35 | }
36 |
37 | @media screen and (min-width: 640px) {
38 | body {
39 | margin: 8px;
40 | max-height: calc(100vh - 16px);
41 | display: flex;
42 | flex-direction: column;
43 | overflow: hidden
44 | }
45 |
46 | #image-viewer {
47 | flex: 1;
48 | display: flex;
49 | overflow: auto;
50 | }
51 |
52 | /* Fit in full height */
53 | .column {
54 | padding: 0.5em;
55 | display: flex;
56 | flex-direction: column;
57 | }
58 |
59 | .column > .content {
60 | max-height: 100%;
61 | overflow: auto;
62 | }
63 |
64 | /* Layout for left, middle, and right */
65 | #detail {
66 | width: 62ch;
67 | }
68 |
69 | #file-tree {
70 | min-width: 12rem;
71 | }
72 |
73 | #grid {
74 | flex: 55%;
75 | display: flex;
76 | }
77 |
78 | /* scroll for block grid */
79 | #block-grid {
80 | flex-grow: 1;
81 | display: flex;
82 | max-height: 100%;
83 | flex-direction: column;
84 | overflow: auto;
85 | }
86 |
87 | #block-grid > .content {
88 | max-height: 100%;
89 | overflow: auto;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/css/inode.css:
--------------------------------------------------------------------------------
1 | #inode-container > .unused-inode {
2 | background-color: gray;
3 | }
4 |
5 | #inode-container > .directory-inode {
6 | background-color: deepskyblue;
7 | }
8 |
9 | #inode-container > .file-inode {
10 | background-color: #4caf50;
11 | }
12 |
13 | #inode-container > .device-inode {
14 | background-color: #ee5b69;
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/fs/Badinode:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Badinode
--------------------------------------------------------------------------------
/fs/Good:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Good
--------------------------------------------------------------------------------
/fs/Goodlarge:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Goodlarge
--------------------------------------------------------------------------------
/fs/Goodlink:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Goodlink
--------------------------------------------------------------------------------
/fs/Goodrefcnt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Goodrefcnt
--------------------------------------------------------------------------------
/fs/Goodrm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Goodrm
--------------------------------------------------------------------------------
/fs/Imrkfree:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Imrkfree
--------------------------------------------------------------------------------
/fs/Imrkused:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Imrkused
--------------------------------------------------------------------------------
/fs/Mrkfree:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Mrkfree
--------------------------------------------------------------------------------
/fs/Mrkused:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Mrkused
--------------------------------------------------------------------------------
/fs/Repair:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/fs/Repair
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | xv6 File System Visualizer
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
30 |
31 |
42 |
43 |
44 |
48 |
49 |
50 |
51 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/js/block.js:
--------------------------------------------------------------------------------
1 | let superBlock;
2 | let blockList;
3 | let bitmap;
4 |
5 | class BlockUtils {
6 | static init() {
7 | superBlock = new SuperBlock(1);
8 | blockList = [];
9 |
10 |
11 | blockList.push(new UnusedBlock(0));
12 | blockList.push(superBlock);
13 |
14 | let i = 2;
15 |
16 | while (i < 2 + superBlock.ninodeblocks) {
17 | blockList.push(new InodeBlock(i++));
18 | }
19 |
20 | blockList.push(new UnusedBlock(i++));
21 |
22 | bitmap = new BitmapBlock(i++);
23 | blockList.push(bitmap);
24 |
25 | while (i < superBlock.nblocks)
26 | blockList.push(BlockUtils.isDataBlockEmpty(i) ? new UnusedBlock(i++) : new DataBlock(i++));
27 | }
28 |
29 | static render() {
30 | Elements.blockContainer.innerHTML = "";
31 | blockList.forEach(e => Elements.blockContainer.appendChild(e.getGridElement()));
32 | superBlock.gridElement.onmouseover();
33 | }
34 |
35 | static isDataBlockEmpty(blockNumber) {
36 | return (bitmap.dataView.getUint8(blockNumber / 8) & (1 << blockNumber % 8)) === 0;
37 | }
38 | }
39 |
40 |
41 | class Block extends GridItem {
42 | constructor(blockNumber) {
43 | super();
44 | this.blockNumber = blockNumber;
45 |
46 | this.dataView = new DataView(image, Config.blockSize * blockNumber, Config.blockSize);
47 | this.uint8Array = new Uint8Array(this.dataView.buffer, this.dataView.byteOffset, this.dataView.byteLength);
48 | this.uint32Array = new Uint32Array(this.dataView.buffer, this.dataView.byteOffset, this.dataView.byteLength);
49 | }
50 |
51 | getDetailElement() {
52 | if (this.detailElement) return this.detailElement;
53 |
54 | this.detailElement = document.createElement("div");
55 |
56 | this.detailElement.appendChild(this.getErrorElement());
57 | this.detailElement.appendChild(this.getSummaryElement());
58 | this.detailElement.appendChild(this.getDataElement());
59 |
60 | return this.detailElement;
61 | }
62 |
63 | getSummaryElement() {
64 | const title = document.createElement("h4");
65 | title.innerText = "Contents in hexadecimal: ";
66 | return title;
67 | }
68 |
69 | getDataElement() {
70 | if (this.dataElement) return this.dataElement;
71 | this.dataElement = this.getHexDataElement();
72 | return this.dataElement;
73 | }
74 |
75 | getHexDataElement() {
76 | const element = document.createElement("pre");
77 | element.innerText = Array.from(this.uint32Array)
78 | .map(e => e.toString(16).padStart(8, '0'))
79 | .join(", \t");
80 | return element
81 | }
82 |
83 | getClassName() {
84 | return this.type.toLowerCase().replace(' ', '-');
85 | }
86 |
87 | getTitle() {
88 | return `Block ${this.blockNumber}: ${this.type}`;
89 | }
90 |
91 | isBlockAscii() {
92 | return false;
93 | }
94 | }
95 |
96 |
97 | class SuperBlock extends Block {
98 | constructor(blockNumber) {
99 | super(blockNumber);
100 |
101 | this.size = this.dataView.getUint32(0, true);
102 | this.nblocks = this.dataView.getUint32(4, true);
103 | this.ninodes = this.dataView.getUint32(8, true);
104 | this.ninodeblocks = this.ninodes * Config.inodeSize / Config.blockSize;
105 |
106 | this.type = "Super Block";
107 | }
108 |
109 |
110 | getSummaryElement() {
111 | const node = document.createElement("div");
112 |
113 | const title = document.createElement("h4");
114 | title.innerText = "Metadata: ";
115 | node.appendChild(title);
116 |
117 | const size = document.createElement("p");
118 | size.innerText = "Image size: " + this.size;
119 | node.appendChild(size);
120 |
121 | const nblocks = document.createElement("p");
122 | nblocks.innerText = "Number of blocks: " + this.nblocks;
123 | node.appendChild(nblocks);
124 |
125 | const ninodes = document.createElement("p");
126 | ninodes.innerText = "Number of inodes: " + this.ninodes;
127 | node.appendChild(ninodes);
128 |
129 | node.appendChild(super.getSummaryElement());
130 |
131 | return node;
132 | }
133 |
134 | getGridText() {
135 | return 'S';
136 | }
137 | }
138 |
139 | class BitmapBlock extends Block {
140 | constructor(blockNumber) {
141 | super(blockNumber);
142 | this.type = "Bitmap Block";
143 | }
144 |
145 | getSummaryElement() {
146 | const title = document.createElement("h4");
147 | title.innerText = "Contents in binary: ";
148 | return title;
149 | }
150 |
151 | getDataElement() {
152 | if (this.dataElement) return this.dataElement;
153 |
154 | this.dataElement = document.createElement("pre");
155 | this.dataElement.innerHTML = Array.from(this.uint8Array)
156 | .map(e => e.toString(2).padStart(8, '0'))
157 | .join(", \t");
158 |
159 | return this.dataElement;
160 | }
161 |
162 | getGridText() {
163 | return 'B';
164 | }
165 | }
166 |
167 | class DataBlock extends Block {
168 | constructor(blockNumber) {
169 | super(blockNumber);
170 |
171 | this.belongsToTextFile = false;
172 | this.isDirectoryBlock = false;
173 |
174 | this.type = "Data Block";
175 | this.gridText = 'D';
176 | }
177 |
178 |
179 | isBlockAscii() {
180 | return this.uint8Array.every(e => e < 128);
181 | }
182 |
183 | getSummaryElement() {
184 | const node = document.createElement("div");
185 |
186 | if (this.inode) {
187 | const title = document.createElement("h4");
188 | title.innerText = `Basic information: `;
189 | node.appendChild(title);
190 |
191 |
192 | const inode = document.createElement("p");
193 | inode.innerText = `Used by: inode ${this.inode.inum}`;
194 | node.appendChild(inode);
195 |
196 | const type = document.createElement("p");
197 | type.innerText = `Type: ${this.inode.typeName}`;
198 | node.appendChild(type);
199 |
200 | if (this.inode.pathList.length !== 0) {
201 | const path = document.createElement("p");
202 | path.innerText = `Path: ${this.inode.pathList.join(", ")}`;
203 | node.appendChild(path);
204 |
205 | }
206 | }
207 |
208 |
209 | if (this.isDirectoryBlock || this.belongsToTextFile) {
210 | const content = document.createElement("h4");
211 | content.innerText = "Contents: ";
212 | node.appendChild(content);
213 | } else {
214 | node.appendChild(super.getSummaryElement());
215 | }
216 |
217 | return node;
218 | }
219 |
220 | getDataElement() {
221 | if (this.dataElement)
222 | return this.dataElement;
223 |
224 | if (this.isDirectoryBlock) {
225 | this.dataElement = document.createElement("pre");
226 | const entries = this.getEntries();
227 | this.dataElement.innerHTML = Object.entries(entries).map(([name, inum]) => `${name} → ${inum}`).join("\n");
228 | return this.dataElement;
229 | }
230 |
231 | if (this.belongsToTextFile) {
232 | this.dataElement = document.createElement("pre");
233 | this.dataElement.innerText = new TextDecoder("utf-8").decode(this.dataView).replace(/\0/g, '');
234 | this.dataElement.classList.add("text");
235 | return this.dataElement;
236 | }
237 |
238 | this.dataElement = this.getHexDataElement();
239 |
240 | return this.dataElement;
241 | }
242 |
243 |
244 | getEntries() {
245 | if (this.entries) return this.entries;
246 |
247 | this.entries = {};
248 | for (let i = 0; i < Config.blockSize / Config.entrySize; i++) {
249 | const inum = this.dataView.getUint16(Config.entrySize * i, true);
250 | if (inum === 0) continue;
251 |
252 | const nameOffset = this.dataView.byteOffset + Config.entrySize * i + 2;
253 | const nameArray = new Uint8Array(this.dataView.buffer, nameOffset, Config.entrySize - 2);
254 | const name = new TextDecoder("utf-8").decode(nameArray).replace(/\0/g, '');
255 |
256 | this.entries[name] = inum;
257 | }
258 |
259 | return this.entries;
260 | }
261 |
262 |
263 | getRelatedDOMList() {
264 | return this.inode ? [this.inode.gridElement, ...this.inode.getRelatedDOMList()] : [];
265 | }
266 |
267 | checkError() {
268 | if (!this.inode && !(this instanceof UnusedBlock)) {
269 | return "Bitmap marks block in use but it is not in use."
270 | }
271 |
272 | if (this.inode && this instanceof UnusedBlock) {
273 | return "Block used by inode but marked free in bitmap."
274 | }
275 | }
276 |
277 | getGridText() {
278 | return 'D';
279 | }
280 | }
281 |
282 |
283 | class InodeBlock extends Block {
284 | constructor(blockNumber) {
285 | super(blockNumber);
286 | this.type = "Inode Block";
287 | }
288 |
289 | getRelatedDOMList() {
290 | const numberOfInodesPerBlock = Config.blockSize / Config.inodeSize;
291 | return [...Array(numberOfInodesPerBlock).keys()]
292 | .map(i => i + numberOfInodesPerBlock * (this.blockNumber - 2))
293 | .map(i => inodeList[i].gridElement);
294 | }
295 |
296 | getGridText() {
297 | return 'I';
298 | }
299 | }
300 |
301 | class UnusedBlock extends DataBlock {
302 | constructor(blockNumber) {
303 | super(blockNumber);
304 | this.type = "Unused Block";
305 | }
306 |
307 | getGridText() {
308 | return '-';
309 | }
310 | }
--------------------------------------------------------------------------------
/js/config.js:
--------------------------------------------------------------------------------
1 | const Config = {
2 | entrySize: 16,
3 | inodeSize: 64,
4 | blockSize: 512,
5 | numberOfDirectAddress: 12,
6 |
7 | imagePath: "fs/",
8 | imageNames: [
9 | "Good", "Goodlink", "Goodrefcnt", "Goodrm", "Goodlarge",
10 | "Repair", "Badinode", "Imrkfree", "Imrkused", "Mrkfree", "Mrkused"]
11 | };
--------------------------------------------------------------------------------
/js/filetree.js:
--------------------------------------------------------------------------------
1 | class FileTree {
2 | static init() {
3 | this.entryList = [];
4 | this.initRoot();
5 | this.traverse(this.root);
6 | }
7 |
8 | static initRoot() {
9 | this.root = {
10 | inum: 1,
11 | indentation: -1,
12 | path: ""
13 | };
14 | inodeList[1].pathList.push("/");
15 | }
16 |
17 | static traverse(parent) {
18 | const entries = inodeList[parent.inum].entries;
19 |
20 | // make sure that "." and ".." appears first
21 | if (entries["."])
22 | this.entryList.push(new Entry(".", entries["."], parent));
23 |
24 | if (entries[".."])
25 | this.entryList.push(new Entry("..", entries[".."], parent));
26 |
27 | for (const [name, inum] of Object.entries(entries)) {
28 | if (name === '.' || name === '..') continue;
29 |
30 | const entry = new Entry(name, inum, parent);
31 | this.entryList.push(entry);
32 |
33 | if (inodeList[inum].type === 1)
34 | this.traverse(entry);
35 | }
36 |
37 | }
38 |
39 | static render() {
40 | Elements.fileTreeContent.innerHTML = '';
41 | this.entryList.forEach(e => Elements.fileTreeContent.appendChild(e.getElement()));
42 | }
43 | }
44 |
45 |
46 | class Entry {
47 | constructor(name, inum, parent) {
48 | this.name = name;
49 | this.inum = inum;
50 | this.inode = inodeList[inum];
51 |
52 | this.indentation = parent.indentation + 1;
53 | this.path = parent.path + "/" + name;
54 |
55 | if (name !== '.' && name !== '..')
56 | this.inode.pathList.push(this.path);
57 | }
58 |
59 | getElement() {
60 | if (this.element) return this.element;
61 |
62 | this.element = document.createElement("pre");
63 | this.element.innerText = `${this.name} → ${this.inum}`;
64 |
65 | if (this.indentation)
66 | this.element.style.marginLeft = this.indentation + "em";
67 |
68 | this.element.onmouseover = this.inode.gridElement.onmouseover;
69 | this.element.onclick = this.inode.gridElement.onclick;
70 | this.inode.fileTreeDOMList.push(this.element);
71 |
72 | return this.element;
73 | }
74 | }
--------------------------------------------------------------------------------
/js/grid.js:
--------------------------------------------------------------------------------
1 | class Grid {
2 | static init() {
3 | Grid.enableHover = true;
4 | Elements.fileTreePanel.onclick = Grid.resetHover;
5 | Elements.gridColumn.onclick = Grid.resetHover;
6 | }
7 |
8 | static setActive(newActiveElem) {
9 | Grid.removeOldActiveElem();
10 | Grid.activeElem = newActiveElem;
11 | Grid.setDetailContent();
12 | Grid.showRelated();
13 | }
14 |
15 | static resetHover() {
16 | Grid.enableHover = true;
17 | Grid.removeOldActiveElem();
18 | }
19 |
20 | static removeOldActiveElem() {
21 | if (Grid.activeElem)
22 | Grid.activeElem.gridElement.classList.remove("hovered", "selected");
23 | if (Grid.relatedDOMList)
24 | Grid.relatedDOMList.forEach(e => e.classList.remove("related"));
25 | Elements.blockContainer.classList.remove("not-selected");
26 | Elements.inodeContainer.classList.remove("not-selected");
27 | Elements.fileTreeContent.classList.remove("not-selected")
28 | }
29 |
30 | static setDetailContent() {
31 | Elements.detailTitle.innerText = Grid.activeElem.getTitle();
32 |
33 | if (!Grid.activeElem.detailDOM) Grid.activeElem.detailDOM = Grid.activeElem.getDetailElement();
34 | Elements.detailContent.innerHTML = '';
35 | Elements.detailContent.appendChild(Grid.activeElem.detailDOM);
36 | }
37 |
38 | static showRelated() {
39 | Grid.relatedDOMList = Grid.activeElem.getRelatedDOMList();
40 | Grid.relatedDOMList.forEach(e => e.classList.add("related"));
41 | }
42 |
43 | static setHovered() {
44 | Grid.activeElem.gridElement.classList.add("hovered");
45 | }
46 |
47 | static setClicked() {
48 | Grid.activeElem.gridElement.classList.add("selected");
49 | Elements.blockContainer.classList.add("not-selected");
50 | Elements.inodeContainer.classList.add("not-selected");
51 | Elements.fileTreeContent.classList.add("not-selected")
52 | }
53 | }
54 |
55 | class GridItem {
56 | getTitle() {
57 | }
58 |
59 | getClassName() {
60 | }
61 |
62 | getRelatedDOMList() {
63 | return [];
64 | }
65 |
66 | getDetailElement() {
67 | }
68 |
69 | checkError() {
70 | return false;
71 | }
72 |
73 | getGridText() {
74 | return '-';
75 | }
76 |
77 |
78 | getGridElement() {
79 | if (this.gridElement) return this.gridElement;
80 |
81 | this.gridElement = document.createElement("div");
82 |
83 | // error checking
84 | this.error = this.checkError();
85 | if (this.error) {
86 | this.gridElement.classList.add("error");
87 | this.gridElement.innerHTML = "?";
88 | } else {
89 | this.gridElement.classList.add(this.getClassName());
90 | this.gridElement.innerHTML = this.getGridText();
91 | }
92 |
93 | // set mouse over event
94 | this.gridElement.onmouseover = (e) => {
95 | if (!Grid.enableHover) return;
96 | Grid.setActive(this);
97 | Grid.setHovered();
98 |
99 | if (e) e.stopPropagation();
100 | };
101 |
102 |
103 | // set mouse click event
104 | this.gridElement.onclick = (e) => {
105 | Grid.enableHover = !Grid.enableHover;
106 | Grid.setActive(this);
107 |
108 | if (Grid.enableHover)
109 | Grid.setHovered();
110 | else
111 | Grid.setClicked();
112 |
113 | if (e) e.stopPropagation();
114 | };
115 |
116 | return this.gridElement;
117 | }
118 |
119 | getErrorElement() {
120 | const node = document.createElement("div");
121 |
122 | if (!this.error) return node;
123 |
124 | const errorTitle = document.createElement("h4");
125 | errorTitle.innerText = `Error: `;
126 | node.appendChild(errorTitle);
127 |
128 | const error = document.createElement("p");
129 | error.innerText = this.error;
130 | node.appendChild(error);
131 |
132 | return node;
133 | }
134 | }
--------------------------------------------------------------------------------
/js/image.js:
--------------------------------------------------------------------------------
1 | class Image {
2 | constructor(imageName) {
3 | this.element = document.createElement("div");
4 |
5 | this.inputElement = document.createElement("input");
6 | this.inputElement.name = 'file';
7 | this.inputElement.value = Config.imagePath + imageName;
8 | this.inputElement.type = 'radio';
9 | this.inputElement.id = imageName;
10 | this.inputElement.onchange = () => main(Config.imagePath + imageName);
11 | this.element.appendChild(this.inputElement);
12 |
13 |
14 | const textElement = document.createElement("pre");
15 | textElement.textContent = imageName;
16 |
17 | const labelElement = document.createElement("label");
18 | labelElement.htmlFor = imageName;
19 | labelElement.appendChild(textElement);
20 |
21 | this.element.appendChild(labelElement);
22 | }
23 |
24 | check() {
25 | this.inputElement.checked = true;
26 | this.inputElement.onchange();
27 | }
28 |
29 | uncheck() {
30 | this.inputElement.checked = false;
31 | }
32 | }
33 |
34 | const imageObjects = Config.imageNames.map(imageName => new Image(imageName));
35 |
36 | imageObjects.forEach(image => Elements.imageListContainer.appendChild(image.element));
37 | imageObjects[2].check();
38 |
39 | Elements.fileUpload.onchange = (e) => {
40 | if (e.target.files.length === 0) return;
41 | imageObjects.forEach(e => e.uncheck());
42 | main(e.target.files[0]);
43 | };
--------------------------------------------------------------------------------
/js/index.js:
--------------------------------------------------------------------------------
1 | let image;
2 |
3 | async function main(file) {
4 | image = await loadImage(file);
5 |
6 | Grid.init();
7 | BlockUtils.init();
8 | InodeUtils.init();
9 | FileTree.init();
10 |
11 |
12 | BlockUtils.render();
13 | InodeUtils.render();
14 | FileTree.render();
15 | }
16 |
17 | async function loadImage(file) {
18 | if (file instanceof File) { // local file
19 | const reader = new FileReader();
20 | return await new Promise((resolve) => {
21 | reader.onload = () => resolve(reader.result);
22 | reader.readAsArrayBuffer(file);
23 | });
24 | } else { // remote file
25 | const response = await fetch(file);
26 | return await response.arrayBuffer();
27 | }
28 | }
--------------------------------------------------------------------------------
/js/inode.js:
--------------------------------------------------------------------------------
1 | let inodeList;
2 |
3 | class InodeUtils {
4 | static init() {
5 | inodeList = Array.from(new Array(superBlock.ninodes).keys(), i => new Inode(i));
6 | }
7 |
8 | static render() {
9 | Elements.inodeContainer.innerHTML = "";
10 | inodeList.forEach(e => Elements.inodeContainer.appendChild(e.getGridElement()));
11 | }
12 | }
13 |
14 |
15 | class Inode extends GridItem {
16 | constructor(inum) {
17 | super();
18 |
19 | this.inum = inum;
20 | this.inode = new DataView(image, Config.blockSize * 2 + inum * Config.inodeSize, Config.inodeSize);
21 |
22 | this.type = this.inode.getUint16(0, true);
23 | this.major = this.inode.getUint16(2, true);
24 | this.minor = this.inode.getUint16(4, true);
25 | this.nlink = this.inode.getUint16(6, true);
26 | this.size = this.inode.getUint32(8, true);
27 |
28 | this.typeName = this.getTypeName();
29 | this.pathList = [];
30 | this.fileTreeDOMList = [];
31 |
32 |
33 | // init addresses
34 | const numberOfAddresses = Math.floor((this.size + Config.blockSize - 1) / Config.blockSize);
35 | this.dataAddresses = [];
36 | this.allAddresses = [];
37 |
38 | for (let i = 0; i < Config.numberOfDirectAddress && i < numberOfAddresses; i++) {
39 | const address = this.inode.getUint32(12 + i * 4, true);
40 | this.dataAddresses.push(address);
41 | this.allAddresses.push(address);
42 | }
43 |
44 | if (numberOfAddresses > Config.numberOfDirectAddress) {
45 | const indirectAddress = this.inode.getUint32(12 + Config.numberOfDirectAddress * 4, true);
46 | const indirectBlock = blockList[indirectAddress].dataView;
47 | this.allAddresses.push(indirectAddress);
48 | for (let i = 0; i < numberOfAddresses - Config.numberOfDirectAddress; i++) {
49 | const address = indirectBlock.getUint32(i * 4, true);
50 | this.dataAddresses.push(address);
51 | this.allAddresses.push(address);
52 | }
53 | }
54 |
55 |
56 | // init blocks
57 | this.dataBlocks = this.dataAddresses.map(i => blockList[i]);
58 | this.allBlocks = this.allAddresses.map(i => blockList[i]);
59 | this.allBlocks.forEach(e => e.inode = this);
60 |
61 | if (this.type === 1) {
62 | this.dataBlocks.forEach(e => e.isDirectoryBlock = true);
63 | this.entries = Object.assign({}, ...this.dataBlocks.map(block => block.getEntries()));
64 | } else if (this.dataBlocks.every(e => e.isBlockAscii())) {
65 | this.dataBlocks.forEach(e => e.belongsToTextFile = true);
66 | }
67 |
68 | }
69 |
70 | getTypeName() {
71 | if (this.type > 3) return "Unknown";
72 | return ["Unused", "Directory", "File", "Device"][this.type];
73 | }
74 |
75 | getGridText() {
76 | if (this.type > 3) return "?";
77 | return ["-", "D", "F", "H"][this.type];
78 | }
79 |
80 | getClassName() {
81 | return this.typeName.toLowerCase() + "-inode";
82 | }
83 |
84 | checkError() {
85 | if (this.type > 3)
86 | return "Invalid inode type.";
87 | if (this.type === 0 && this.pathList.length !== 0)
88 | return "Inode referred to in directory but marked free.";
89 | if (this.type !== 0 && this.pathList.length === 0)
90 | return "Inode marked use but not found in a directory.";
91 | }
92 |
93 | getDetailElement() {
94 | if (this.detailElement) return this.detailElement;
95 |
96 | this.detailElement = document.createElement("div");
97 |
98 | this.detailElement.appendChild(this.getErrorElement());
99 |
100 | // title
101 | const title = document.createElement("h4");
102 | title.innerText = `Basic information: `;
103 | this.detailElement.appendChild(title);
104 |
105 | // type
106 | const type = document.createElement("p");
107 | type.innerText = `Type: ${this.type} (${this.typeName})`;
108 | this.detailElement.appendChild(type);
109 |
110 | // path
111 | if (this.pathList.length !== 0) {
112 | const path = document.createElement("p");
113 | path.innerText = "Path: " + this.pathList.join(", ");
114 | this.detailElement.appendChild(path);
115 | }
116 |
117 | // size
118 | if (this.size !== 0 || this.type !== 0) {
119 | const size = document.createElement("p");
120 | size.innerText = "Size: " + this.size;
121 | this.detailElement.appendChild(size);
122 | }
123 |
124 | // nlink
125 | if (this.nlink !== 0 || this.type !== 0) {
126 | const nlink = document.createElement("p");
127 | nlink.innerText = "Number of links: " + this.nlink;
128 | this.detailElement.appendChild(nlink);
129 | }
130 |
131 | // nblock
132 | if (this.type === 1 || this.type === 2) {
133 | const nblock = document.createElement("p");
134 | nblock.innerText = "Number of data blocks: " + this.dataAddresses.length;
135 | this.detailElement.appendChild(nblock);
136 | }
137 |
138 | // device only
139 | if (this.type === 3) {
140 | const major = document.createElement("p");
141 | major.innerText = "Major device number: " + this.major;
142 | this.detailElement.appendChild(major);
143 |
144 |
145 | const minor = document.createElement("p");
146 | minor.innerText = "Minor device number: " + this.minor;
147 | this.detailElement.appendChild(minor);
148 | }
149 |
150 | // data addresses
151 | if (this.allAddresses.length !== 0) {
152 | const dataAddresses = document.createElement("p");
153 | dataAddresses.innerText = "Data block addresses: " + this.dataAddresses.join(", ");
154 | this.detailElement.appendChild(dataAddresses);
155 | }
156 |
157 | // indirect address
158 | if (this.dataAddresses.length > Config.numberOfDirectAddress) {
159 | const indirectAddress = document.createElement("p");
160 | indirectAddress.innerText = "Indirect block address: " + this.allAddresses[Config.numberOfDirectAddress];
161 | this.detailElement.appendChild(indirectAddress);
162 | }
163 |
164 | // data blocks
165 | for (let dataBlock of this.dataBlocks) {
166 | const title = document.createElement("h4");
167 | title.innerText = `Block ${dataBlock.blockNumber}:`;
168 | this.detailElement.appendChild(title);
169 |
170 | this.detailElement.appendChild(dataBlock.getDataElement());
171 | }
172 |
173 | return this.detailElement;
174 | }
175 |
176 | getRelatedDOMList() {
177 | return [...this.allBlocks.map(e => e.gridElement), ...this.fileTreeDOMList];
178 | }
179 |
180 | getTitle() {
181 | return `Inode ${this.inum}: ${this.typeName}`;
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/js/text.min.js:
--------------------------------------------------------------------------------
1 | (function(l){function m(b){b=void 0===b?"utf-8":b;if("utf-8"!==b)throw new RangeError("Failed to construct 'TextEncoder': The encoding label provided ('"+b+"') is invalid.");}function k(b,a){b=void 0===b?"utf-8":b;a=void 0===a?{fatal:!1}:a;if("utf-8"!==b)throw new RangeError("Failed to construct 'TextDecoder': The encoding label provided ('"+b+"') is invalid.");if(a.fatal)throw Error("Failed to construct 'TextDecoder': the 'fatal' option is unsupported.");}if(l.TextEncoder&&l.TextDecoder)return!1;
2 | Object.defineProperty(m.prototype,"encoding",{value:"utf-8"});m.prototype.encode=function(b,a){a=void 0===a?{stream:!1}:a;if(a.stream)throw Error("Failed to encode: the 'stream' option is unsupported.");a=0;for(var h=b.length,f=0,c=Math.max(32,h+(h>>1)+7),e=new Uint8Array(c>>3<<3);a=d){if(a=d)continue}f+4>e.length&&(c+=8,c*=1+a/b.length*2,c=c>>3<<3,
3 | g=new Uint8Array(c),g.set(e),e=g);if(0===(d&4294967168))e[f++]=d;else{if(0===(d&4294965248))e[f++]=d>>6&31|192;else if(0===(d&4294901760))e[f++]=d>>12&15|224,e[f++]=d>>6&63|128;else if(0===(d&4292870144))e[f++]=d>>18&7|240,e[f++]=d>>12&63|128,e[f++]=d>>6&63|128;else continue;e[f++]=d&63|128}}return e.slice(0,f)};Object.defineProperty(k.prototype,"encoding",{value:"utf-8"});Object.defineProperty(k.prototype,"fatal",{value:!1});Object.defineProperty(k.prototype,"ignoreBOM",{value:!1});k.prototype.decode=
4 | function(b,a){a=void 0===a?{stream:!1}:a;if(a.stream)throw Error("Failed to decode: the 'stream' option is unsupported.");b=new Uint8Array(b);a=0;for(var h=b.length,f=[];a>>10&1023|55296),c=56320|
5 | c&1023);f.push(c)}}return String.fromCharCode.apply(null,f)};l.TextEncoder=m;l.TextDecoder=k})("undefined"!==typeof window?window:"undefined"!==typeof global?global:this);
6 |
--------------------------------------------------------------------------------
/screenrecord.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/screenrecord.gif
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShawnZhong/xv6-file-system-visualizer/4f972d871e8bacb94e7d8f5cf57bbceac7ea177d/screenshot.png
--------------------------------------------------------------------------------