| 1 | const path = require('path'); |
| 2 | |
| 3 | const NAME_RE = /^[a-z0-9][a-z0-9_-]{0,38}$/; |
| 4 | |
| 5 | const RESERVED = new Set([ |
| 6 | 'admin', 'api', 'login', 'logout', 'signup', 'me', 'static', 'public', |
| 7 | 'settings', 'help', 'about', 'data', 'lib', 'views', 'scripts', 'new', |
| 8 | 'health', 'favicon.ico', 'robots.txt' |
| 9 | ]); |
| 10 | |
| 11 | function isValidName(s) { |
| 12 | if (typeof s !== 'string') return false; |
| 13 | if (!NAME_RE.test(s)) return false; |
| 14 | if (RESERVED.has(s)) return false; |
| 15 | return true; |
| 16 | } |
| 17 | |
| 18 | function isReservedName(s) { return RESERVED.has(s); } |
| 19 | |
| 20 | function safeJoin(rootAbs, ...segments) { |
| 21 | const rootResolved = path.resolve(rootAbs); |
| 22 | const joined = path.resolve(rootResolved, ...segments); |
| 23 | if (joined !== rootResolved && !joined.startsWith(rootResolved + path.sep)) { |
| 24 | throw new Error('path escape attempted: ' + joined); |
| 25 | } |
| 26 | return joined; |
| 27 | } |
| 28 | |
| 29 | function isVisibility(v) { return v === 'public' || v === 'private'; } |
| 30 | |
| 31 | function nonEmptyString(v, max) { |
| 32 | if (typeof v !== 'string' || v.length === 0) return false; |
| 33 | if (max != null && v.length > max) return false; |
| 34 | return true; |
| 35 | } |
| 36 | |
| 37 | module.exports = { isValidName, isReservedName, safeJoin, isVisibility, nonEmptyString }; |
| 38 | |