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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 17 additions & 28 deletions book/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,32 +180,20 @@ bookSidebar:
id: programmability/display
- label: Events
id: programmability/events
# - label: Balance & Coin
# id: programmability/balance-and-coin
# type: doc
- label: Balance & Coin
id: programmability/balance-and-coin
- label: 'Pattern: Hot Potato'
id: programmability/hot-potato-pattern
# - label: '8.18 Pattern: Request'
Comment thread
jessiemongeon1 marked this conversation as resolved.
# id: programmability/request-pattern
# type: doc
# - label: '8.19 Pattern: Object Capability'
# id: programmability/object-capability-pattern
# type: doc
# - label: '8.20 Package Upgrades'
# id: programmability/package-upgrades
# type: doc
# - label: '8.21 Transaction Blocks'
# id: programmability/transaction-block
# type: doc
# - label: '8.22 Authorization Patterns'
# id: programmability/authorization-patterns
# type: doc
# - label: '8.23 Cryptography and Hashing'
# id: programmability/cryptography-and-hashing
# type: doc
# - label: '8.24 Randomness'
# id: programmability/cryptography-and-hashing
# type: doc
- label: 'Pattern: Object Capability'
id: programmability/object-capability
- label: Authorization Patterns
id: programmability/authorization-patterns
- label: Cryptography and Hashing
id: programmability/cryptography-and-hashing
- label: Randomness
id: programmability/randomness
- label: Fast Path
id: programmability/fast-path
- label: BCS
id: programmability/bcs
- type: category
Expand Down Expand Up @@ -249,6 +237,9 @@ bookSidebar:
- type: category
label: Guides
enumerate: false
link:
id: guides/index
type: doc
items:
- label: 2024 Migration Guide
id: guides/2024-migration-guide
Expand All @@ -260,10 +251,8 @@ bookSidebar:
id: guides/better-error-handling
- label: Code Quality Checklist
id: guides/code-quality-checklist
# - label: Open-sourcing Libraries
# id: guides/open-sourcing-libraries
# - label: Creating an NFT Collection
# id: guides/creating-an-nft-collection
Comment thread
jessiemongeon1 marked this conversation as resolved.
- label: Open-sourcing Libraries
id: guides/open-sourcing-libraries
- type: category
label: Appendix
enumerate: false
Expand Down
2 changes: 1 addition & 1 deletion reference/index-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fun borrow_first(input: &Vs): &u64 {
&input.vs[0].v[0]
// translates to `vector::borrow(&vector::borrow(&input.vs, 0).v, 0)`
}
````
```

### Index Functions Take Flexible Arguments

Expand Down
10 changes: 8 additions & 2 deletions reference/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,14 @@ referenceSidebar:
id: constants
- label: 11. Generics
id: generics
- label: 12. Abilities
id: abilities
- type: category
Comment thread
jessiemongeon1 marked this conversation as resolved.
Outdated
label: 12. Abilities
link:
id: abilities
type: doc
items:
- label: 12.1 Sui Object
id: abilities/object
- label: 13. Uses and Aliases
id: uses
- type: category
Expand Down
240 changes: 240 additions & 0 deletions site/serve-with-rewrites.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/*
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
*/

/**
* Local server that serves the Docusaurus build with proper headers
* for markdown files, llms.txt, and content negotiation.
*
* Usage: node serve-with-rewrites.js [port]
*/

const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');

const PORT = process.argv[2] || 3001;
const BUILD_DIR = path.join(__dirname, 'build');
const BUILD_ROOT = path.resolve(BUILD_DIR);
const BUILD_ROOT_WITH_SEP = BUILD_ROOT.endsWith(path.sep) ? BUILD_ROOT : BUILD_ROOT + path.sep;
const CANONICAL_BUILD_ROOT = fs.realpathSync(BUILD_ROOT);
const CANONICAL_BUILD_ROOT_WITH_SEP = CANONICAL_BUILD_ROOT.endsWith(path.sep)
? CANONICAL_BUILD_ROOT
: CANONICAL_BUILD_ROOT + path.sep;

const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.md': 'text/markdown; charset=utf-8',
'.txt': 'text/plain; charset=utf-8',
'.ebnf': 'text/plain; charset=utf-8',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.pdf': 'application/pdf',
};

function getContentType(filePath) {
const ext = path.extname(filePath).toLowerCase();
return MIME_TYPES[ext] || 'application/octet-stream';
}

function getCacheControl(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (ext === '.html' || ext === '.txt' || ext === '.md' || ext === '.ebnf') {
return 'public, max-age=0, must-revalidate';
}
return 'public, max-age=3600';
}

/**
* Checks whether the request Accept header includes text/markdown.
*/
function acceptsMarkdown(req) {
const accept = req.headers['accept'] || '';
return accept.includes('text/markdown');
}

function isCanonicalPathUnderBuildRoot(targetPath) {
try {
const realTarget = fs.realpathSync.native ? fs.realpathSync.native(targetPath) : fs.realpathSync(targetPath);
const realBuildRoot = fs.realpathSync.native ? fs.realpathSync.native(BUILD_ROOT) : fs.realpathSync(BUILD_ROOT);
const realBuildRootWithSep = realBuildRoot.endsWith(path.sep) ? realBuildRoot : realBuildRoot + path.sep;
return realTarget === realBuildRoot || realTarget.startsWith(realBuildRootWithSep);
} catch (e) {
return false;
}
}

function isSafeRequestPathname(pathname) {
if (typeof pathname !== 'string' || pathname.length === 0) return false;
if (!pathname.startsWith('/')) return false;
if (pathname.includes('\0') || pathname.includes('\\')) return false;

const segments = pathname.split('/');
for (const segment of segments) {
if (segment === '.' || segment === '..') return false;
}

return true;
}

function resolveUnderBuildDir(relativePath) {
let decodedPath;
try {
decodedPath = decodeURIComponent(relativePath || '/');
} catch (e) {
return null;
}

const normalizedPath = decodedPath.replace(/\\/g, '/');
const resolved = path.resolve(BUILD_ROOT, '.' + normalizedPath);
if (resolved !== BUILD_ROOT && !resolved.startsWith(BUILD_ROOT_WITH_SEP)) {
return null;
}

let canonicalResolved;
try {
canonicalResolved = fs.realpathSync(resolved);
} catch (e) {
return null;
}

if (
canonicalResolved !== CANONICAL_BUILD_ROOT &&
!canonicalResolved.startsWith(CANONICAL_BUILD_ROOT_WITH_SEP)
) {
return null;
}

return canonicalResolved;
}

/**
* Tries to resolve a markdown file for the given pathname.
* Maps e.g. "/" -> "index.md", "/foreword" -> "foreword.md",
* "/move-basics/module" -> "move-basics/module.md".
*/
function resolveMarkdownFile(pathname) {
const clean = pathname.replace(/\/+$/, '') || '/';

if (clean === '/') {
const candidate = resolveUnderBuildDir('/index.md');
if (candidate && fs.existsSync(candidate)) return candidate;
return null;
}

// Try <path>.md first, then <path>/index.md
const asMd = resolveUnderBuildDir(clean + '.md');
if (asMd && fs.existsSync(asMd)) return asMd;
Comment thread
jessiemongeon1 marked this conversation as resolved.
Dismissed

const asIndex = resolveUnderBuildDir(path.join(clean, 'index.md'));
if (asIndex && fs.existsSync(asIndex)) return asIndex;

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
Comment thread
jessiemongeon1 marked this conversation as resolved.
Dismissed

return null;
}

const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url);
let pathname = parsedUrl.pathname;

if (!isSafeRequestPathname(pathname)) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('400 Bad Request');
return;
}

// Content negotiation: serve markdown when Accept: text/markdown
if (acceptsMarkdown(req)) {
const mdFile = resolveMarkdownFile(pathname);
if (mdFile) {
const content = fs.readFileSync(mdFile);
Comment thread
jessiemongeon1 marked this conversation as resolved.
Dismissed
res.writeHead(200, {
'Content-Type': 'text/markdown; charset=utf-8',
'Content-Disposition': 'inline',
'Cache-Control': 'public, max-age=0, must-revalidate',
});
res.end(content);
return;
}
}

// Resolve file path
let filePath = resolveUnderBuildDir(pathname);
if (!filePath) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('403 Forbidden');
return;
}

if (!fs.existsSync(filePath)) {
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
jessiemongeon1 marked this conversation as resolved.
Dismissed
// Try index.html for directory-style routes
const indexPath = resolveUnderBuildDir(path.join(pathname, 'index.html'));
if (indexPath && fs.existsSync(indexPath)) {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
Comment thread
jessiemongeon1 marked this conversation as resolved.
Dismissed
filePath = indexPath;
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
return;
}
} else {
let canonicalFilePath;
try {
canonicalFilePath = fs.realpathSync(filePath);
} catch (e) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
return;
}

if (canonicalFilePath !== BUILD_ROOT && !canonicalFilePath.startsWith(BUILD_ROOT_WITH_SEP)) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('403 Forbidden');
return;
}

filePath = canonicalFilePath;

if (fs.statSync(filePath).isDirectory()) {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
Comment thread
jessiemongeon1 marked this conversation as resolved.
Dismissed
const directoryIndexPath = resolveUnderBuildDir(path.join(pathname, 'index.html'));
if (!directoryIndexPath) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('403 Forbidden');
return;
}
filePath = directoryIndexPath;
}
}

if (!fs.existsSync(filePath)) {
Comment thread
jessiemongeon1 marked this conversation as resolved.
Dismissed
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
return;
}

if (!isCanonicalPathUnderBuildRoot(filePath)) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('403 Forbidden');
return;
}

const content = fs.readFileSync(filePath);
Comment thread
jessiemongeon1 marked this conversation as resolved.
Dismissed
res.writeHead(200, {
'Content-Type': getContentType(filePath),
'Content-Disposition': 'inline',
'Cache-Control': getCacheControl(filePath),
});
res.end(content);
});

server.listen(PORT, () => {
console.log(`Serving build at http://localhost:${PORT}/`);
});
Loading
Loading