├── .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 | # ![screenshot](screenshot.png) 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 |
18 | Please select an xv6 file system image: 19 | 20 |
21 | , or upload your own 22 | 23 |
24 | 25 |
26 |
27 |

File Tree

28 |
29 |
30 | 31 |
32 |
33 |

Inodes

34 |
35 |
36 | 37 |
38 |

Blocks

39 |
40 |
41 |
42 | 43 | 44 |
45 |

46 |
47 |
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 --------------------------------------------------------------------------------