koa-classic-server 2.6.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CLAUDE.md +101 -0
  2. package/README.md +564 -591
  3. package/__tests__/benchmark-results-v3.0.0.txt +372 -0
  4. package/__tests__/benchmark.js +1 -1
  5. package/__tests__/caching-headers.test.js +30 -30
  6. package/__tests__/compression-fixtures/data.json +1 -0
  7. package/__tests__/compression-fixtures/large.txt +1 -0
  8. package/__tests__/compression-fixtures/small.txt +1 -0
  9. package/__tests__/compression.test.js +284 -0
  10. package/__tests__/customTest/serversToLoad.util.js +5 -5
  11. package/__tests__/demo-regex-index.js +4 -4
  12. package/__tests__/deprecation-warnings.test.js +71 -183
  13. package/__tests__/directory-sorting-links.test.js +1 -1
  14. package/__tests__/dt-unknown.test.js +39 -28
  15. package/__tests__/ejs.test.js +1 -1
  16. package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
  17. package/__tests__/hidden-fixtures/.env +2 -0
  18. package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
  19. package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
  20. package/__tests__/hidden-fixtures/data.key +1 -0
  21. package/__tests__/hidden-fixtures/file.secret +1 -0
  22. package/__tests__/hidden-fixtures/index.html +1 -0
  23. package/__tests__/hidden-fixtures/normal.txt +1 -0
  24. package/__tests__/hidden-fixtures/subdir/.env +1 -0
  25. package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
  26. package/__tests__/hidden-option.test.js +407 -0
  27. package/__tests__/hideExtension.test.js +70 -13
  28. package/__tests__/index-option.test.js +18 -16
  29. package/__tests__/index.test.js +14 -10
  30. package/__tests__/listing.test.js +437 -0
  31. package/__tests__/logger.test.js +232 -0
  32. package/__tests__/range-fixtures/sample.txt +1 -0
  33. package/__tests__/range.test.js +223 -0
  34. package/__tests__/security-headers.test.js +165 -0
  35. package/__tests__/security.test.js +148 -162
  36. package/__tests__/server-cache-fixtures/large.txt +1 -0
  37. package/__tests__/server-cache-fixtures/small.txt +1 -0
  38. package/__tests__/server-cache.test.js +594 -0
  39. package/__tests__/symlink.test.js +18 -15
  40. package/__tests__/template-timeout.test.js +321 -0
  41. package/docs/ACTION_PLAN.md +293 -0
  42. package/docs/CHANGELOG.md +289 -0
  43. package/docs/CODE_REVIEW.md +2 -0
  44. package/docs/DOCUMENTATION.md +259 -32
  45. package/docs/EXAMPLES_INDEX_OPTION.md +3 -3
  46. package/docs/FLOW_DIAGRAM.md +15 -13
  47. package/docs/INDEX_OPTION_PRIORITY.md +2 -2
  48. package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
  49. package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
  50. package/docs/PERFORMANCE_COMPARISON.md +7 -7
  51. package/docs/security_improvement_for_V3.md +421 -0
  52. package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +5 -5
  53. package/docs/template-engine/esempi-incrementali.js +1 -1
  54. package/eslint.config.mjs +17 -0
  55. package/index.cjs +1507 -429
  56. package/index.mjs +1 -5
  57. package/package.json +9 -1
@@ -1,7 +1,7 @@
1
1
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2
2
  //
3
3
  // SECURITY & BUG TESTS
4
- // Questi test verificano le vulnerabilità e i bug identificati nel DEBUG_REPORT.md
4
+ // These tests verify the security properties and bug fixes documented in DEBUG_REPORT.md
5
5
  //
6
6
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7
7
 
@@ -13,6 +13,8 @@ const path = require('path');
13
13
 
14
14
  const rootDir = path.join(__dirname, 'publicWwwTest');
15
15
 
16
+ // ─── Path Traversal ───────────────────────────────────────────────────────────
17
+
16
18
  describe('Security Tests - Path Traversal', () => {
17
19
  let app;
18
20
  let server;
@@ -20,44 +22,56 @@ describe('Security Tests - Path Traversal', () => {
20
22
  beforeAll(() => {
21
23
  app = new Koa();
22
24
  app.use(koaClassicServer(rootDir, {
23
- showDirContents: true
25
+ dirListing: { enabled: true }
24
26
  }));
25
27
  server = app.listen();
26
28
  });
27
29
 
28
- test('VULNERABILITY: Path traversal con ../ dovrebbe essere bloccato', async () => {
29
- // Tenta di accedere al file package.json che è fuori da publicWwwTest
30
- const res = await supertest(server).get('/../package.json');
30
+ afterAll(() => {
31
+ server.close();
32
+ });
31
33
 
32
- // Il file NON dovrebbe essere accessibile
33
- // ATTUALMENTE FALLISCE - questa è la vulnerabilità!
34
- // expect(res.status).toBe(403); // Dovrebbe essere forbidden
35
- // expect(res.text).not.toContain('"name"'); // Non dovrebbe vedere il contenuto
34
+ // On Linux, path.normalize() cannot escape '/' (the OS root), so '/../package.json'
35
+ // resolves to rootDir/package.json which simply does not exist → 404.
36
+ // On Windows, backslash sequences can escape rootDir and the startsWith() check
37
+ // fires → 403. Both outcomes prove the traversal was blocked (no 200 with file content).
36
38
 
37
- // Per ora verifichiamo che la vulnerabilità esista
38
- console.log('⚠️ Path Traversal Test - Status:', res.status);
39
- // Se vedi status 200 e contenuto di package.json, la vulnerabilità è confermata
39
+ test('../ traversal is blocked (403 or 404, never 200 with file content)', async () => {
40
+ const res = await supertest(server).get('/../package.json');
41
+ expect([403, 404]).toContain(res.status);
42
+ expect(res.text).not.toContain('"name"');
40
43
  });
41
44
 
42
- test('VULNERABILITY: Path traversal con encoding dovrebbe essere bloccato', async () => {
43
- // Tenta con encoding URL
45
+ test('URL-encoded ../ traversal (%2e%2e%2f) is blocked (403 or 404)', async () => {
44
46
  const res = await supertest(server).get('/%2e%2e%2f%2e%2e%2fpackage.json');
45
-
46
- console.log('⚠️ Path Traversal Encoded Test - Status:', res.status);
47
+ expect([403, 404]).toContain(res.status);
48
+ expect(res.text).not.toContain('"name"');
47
49
  });
48
50
 
49
- test('VULNERABILITY: Path traversal assoluto dovrebbe essere bloccato', async () => {
50
- // Tenta path assoluto
51
+ test('multi-level traversal (/../../../etc/hosts) is blocked (403 or 404)', async () => {
51
52
  const res = await supertest(server).get('/../../../etc/hosts');
53
+ expect([403, 404]).toContain(res.status);
54
+ });
52
55
 
53
- console.log('⚠️ Path Traversal Absolute Test - Status:', res.status);
56
+ test('null byte in path is rejected with 400', async () => {
57
+ // path.normalize() throws ERR_INVALID_ARG_VALUE for paths with \0;
58
+ // the null byte guard returns 400 before reaching fs operations.
59
+ const res = await supertest(server).get('/file%00.txt');
60
+ expect(res.status).toBe(400);
54
61
  });
55
62
 
56
- afterAll(() => {
57
- server.close();
63
+ test('backslash sequences are not treated as path separators on Linux', async () => {
64
+ // On Linux, \\ is a literal filename character, not a path separator.
65
+ // path.normalize leaves it intact and the resolved path stays within rootDir.
66
+ // The file simply does not exist → 404 (not a traversal escape).
67
+ const res = await supertest(server).get('/..%5C..%5Cetc%5Chosts');
68
+ expect([403, 404]).toContain(res.status);
69
+ // On Windows, path.normalize converts \ to / and the traversal check catches it → 403.
58
70
  });
59
71
  });
60
72
 
73
+ // ─── Status Code 404 ─────────────────────────────────────────────────────────
74
+
61
75
  describe('Bug Tests - Status Code 404', () => {
62
76
  let app;
63
77
  let server;
@@ -65,34 +79,25 @@ describe('Bug Tests - Status Code 404', () => {
65
79
  beforeAll(() => {
66
80
  app = new Koa();
67
81
  app.use(koaClassicServer(rootDir, {
68
- showDirContents: true
82
+ dirListing: { enabled: true }
69
83
  }));
70
84
  server = app.listen();
71
85
  });
72
86
 
73
- test('FIXED: File inesistente dovrebbe restituire status 404', async () => {
87
+ test('FIXED: Non-existent file returns 404', async () => {
74
88
  const res = await supertest(server).get('/file-che-non-esiste-xyz123.txt');
75
-
76
- console.log('✅ 404 Status Test - Status Code:', res.status);
77
- console.log(' Expected: 404, Got:', res.status);
78
-
79
- // FIXED: Now returns proper 404 status
80
89
  expect(res.status).toBe(404);
81
90
  expect(res.text).toContain('Not Found');
82
91
  });
83
92
 
84
- test('FIXED: Directory con showDirContents=false dovrebbe restituire 404', async () => {
93
+ test('FIXED: Directory with dirListing.enabled=false returns 404', async () => {
85
94
  const app2 = new Koa();
86
95
  app2.use(koaClassicServer(rootDir, {
87
- showDirContents: false
96
+ dirListing: { enabled: false }
88
97
  }));
89
98
  const server2 = app2.listen();
90
99
 
91
100
  const res = await supertest(server2).get('/');
92
-
93
- console.log('✅ 404 Directory Test - Status Code:', res.status);
94
-
95
- // FIXED: Now returns proper 404 status
96
101
  expect(res.status).toBe(404);
97
102
 
98
103
  server2.close();
@@ -103,11 +108,12 @@ describe('Bug Tests - Status Code 404', () => {
103
108
  });
104
109
  });
105
110
 
111
+ // ─── Template Rendering Errors ────────────────────────────────────────────────
112
+
106
113
  describe('Bug Tests - Template Rendering Errors', () => {
107
- test('BUG: Template render error dovrebbe essere gestito, non crashare', async () => {
114
+ test('FIXED: Template render error returns 500 HTML page, server does not crash', async () => {
108
115
  const app = new Koa();
109
116
 
110
- // Template che lancia errore
111
117
  const brokenRender = async (ctx, next, filePath) => {
112
118
  throw new Error('Simulated template rendering error');
113
119
  };
@@ -115,39 +121,29 @@ describe('Bug Tests - Template Rendering Errors', () => {
115
121
  app.use(koaClassicServer(rootDir, {
116
122
  template: {
117
123
  render: brokenRender,
118
- ext: ['txt'] // Usa .txt per test
124
+ ext: ['txt']
119
125
  }
120
126
  }));
121
127
 
122
128
  const server = app.listen();
123
-
124
- // Crea un file .txt per il test
125
129
  const testFile = path.join(rootDir, 'test-template.txt');
126
130
  fs.writeFileSync(testFile, 'test content');
127
131
 
128
132
  try {
129
133
  const res = await supertest(server).get('/test-template.txt');
130
134
 
131
- console.log('🐛 Template Error Test - Status:', res.status);
132
-
133
- // Dovrebbe gestire l'errore e restituire 500
134
- // expect(res.status).toBe(500);
135
-
136
- // ATTUALMENTE POTREBBE CRASHARE IL SERVER
137
- // Se arriviamo qui senza crash, il test passa
138
- console.log(' Server did not crash (good)');
139
- } catch (error) {
140
- console.log('⚠️ Template error caused request to fail:', error.message);
135
+ expect(res.status).toBe(500);
136
+ expect(res.headers['content-type']).toMatch(/html/);
137
+ expect(res.text).toContain('Internal Server Error');
141
138
  } finally {
142
- // Cleanup
143
- if (fs.existsSync(testFile)) {
144
- fs.unlinkSync(testFile);
145
- }
139
+ if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
146
140
  server.close();
147
141
  }
148
142
  });
149
143
  });
150
144
 
145
+ // ─── File Extension Extraction ────────────────────────────────────────────────
146
+
151
147
  describe('Bug Tests - File Extension Extraction', () => {
152
148
  let app;
153
149
  let server;
@@ -155,10 +151,7 @@ describe('Bug Tests - File Extension Extraction', () => {
155
151
  beforeAll(() => {
156
152
  app = new Koa();
157
153
 
158
- let renderCalled = false;
159
154
  const trackingRender = async (ctx, next, filePath) => {
160
- renderCalled = true;
161
- ctx.renderCalled = true;
162
155
  ctx.body = 'Rendered: ' + path.basename(filePath);
163
156
  };
164
157
 
@@ -172,134 +165,96 @@ describe('Bug Tests - File Extension Extraction', () => {
172
165
  server = app.listen();
173
166
  });
174
167
 
175
- test('BUG: File senza estensione non dovrebbe attivare template rendering', async () => {
176
- // Crea file senza estensione
168
+ afterAll(() => {
169
+ server.close();
170
+ });
171
+
172
+ test('FIXED: File without extension does not trigger template rendering', async () => {
177
173
  const testFile = path.join(rootDir, 'README');
178
174
  fs.writeFileSync(testFile, 'readme content');
179
175
 
180
176
  try {
181
177
  const res = await supertest(server).get('/README');
182
-
183
- console.log('🐛 No Extension Test - Render called?', res.text.includes('Rendered'));
184
-
185
- // Non dovrebbe essere renderizzato
186
- expect(res.text).not.toContain('Rendered');
178
+ const responseBody = res.text !== undefined ? res.text : res.body.toString('utf8');
179
+ expect(responseBody).not.toContain('Rendered');
187
180
  } finally {
188
- if (fs.existsSync(testFile)) {
189
- fs.unlinkSync(testFile);
190
- }
181
+ if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
191
182
  }
192
183
  });
193
184
 
194
- test('BUG: File nascosto Unix non dovrebbe essere trattato con estensione sbagliata', async () => {
195
- // Crea file nascosto
185
+ test('Unix hidden file (.gitignore) is served as a regular file, not rendered', async () => {
186
+ // dotFiles are hidden by default; make them visible for this test so we can
187
+ // verify that the extension check (not the template renderer) handles the file.
188
+ const appVisible = new Koa();
189
+ appVisible.use(koaClassicServer(rootDir, {
190
+ hidden: { dotFiles: { default: 'visible' } },
191
+ template: {
192
+ render: async (ctx, next, filePath) => { ctx.body = 'Rendered: ' + path.basename(filePath); },
193
+ ext: ['txt']
194
+ }
195
+ }));
196
+ const serverVisible = appVisible.listen();
197
+
196
198
  const testFile = path.join(rootDir, '.gitignore');
197
199
  fs.writeFileSync(testFile, 'node_modules/');
198
200
 
199
201
  try {
200
- const res = await supertest(server).get('/.gitignore');
201
-
202
- console.log('🐛 Hidden File Test - Status:', res.status);
203
-
204
- // .gitignore non ha estensione .txt, non dovrebbe essere renderizzato
205
- // Ma con il bug attuale, potrebbe essere processato come estensione "gitignore"
202
+ const res = await supertest(serverVisible).get('/.gitignore');
203
+ // .gitignore has no .txt extension — template must not be invoked
204
+ expect(res.status).toBe(200);
205
+ const responseBody = res.text !== undefined ? res.text : res.body.toString('utf8');
206
+ expect(responseBody).not.toContain('Rendered');
207
+ expect(responseBody).toContain('node_modules/');
206
208
  } finally {
207
- if (fs.existsSync(testFile)) {
208
- fs.unlinkSync(testFile);
209
- }
209
+ if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
210
+ serverVisible.close();
210
211
  }
211
212
  });
212
-
213
- afterAll(() => {
214
- server.close();
215
- });
216
213
  });
217
214
 
218
- describe('Bug Tests - Race Condition File Access', () => {
219
- test('BUG: File cancellato tra check ed access dovrebbe essere gestito', async () => {
220
- const app = new Koa();
221
- app.use(koaClassicServer(rootDir));
222
- const server = app.listen();
223
-
224
- // Crea file temporaneo
225
- const testFile = path.join(rootDir, 'temp-race-test.txt');
226
- fs.writeFileSync(testFile, 'temporary content');
227
-
228
- // Simula race condition: cancella il file appena dopo la richiesta
229
- setTimeout(() => {
230
- if (fs.existsSync(testFile)) {
231
- fs.unlinkSync(testFile);
232
- }
233
- }, 5);
234
-
235
- try {
236
- const res = await supertest(server).get('/temp-race-test.txt');
237
-
238
- console.log('🐛 Race Condition Test - Status:', res.status);
215
+ // ─── Race Condition File Access ───────────────────────────────────────────────
216
+ //
217
+ // BEHAVIOUR GUARANTEE (not tested — timing is non-deterministic):
218
+ // If a file is deleted between the access check and the stream open,
219
+ // the stream error handler fires and — if headers have not been sent yet —
220
+ // the server returns 500. When rawFile cache is warm the race cannot occur
221
+ // because the file is served entirely from memory.
222
+ //
223
+ // ─────────────────────────────────────────────────────────────────────────────
239
224
 
240
- // Dovrebbe gestire l'errore gracefully
241
- // expect(res.status).toBe(404) o 500;
242
- } catch (error) {
243
- console.log('⚠️ Race condition caused error:', error.message);
244
- } finally {
245
- // Cleanup
246
- if (fs.existsSync(testFile)) {
247
- fs.unlinkSync(testFile);
248
- }
249
- server.close();
250
- }
251
- });
252
- });
225
+ // ─── Directory Read Errors ────────────────────────────────────────────────────
253
226
 
254
227
  describe('Bug Tests - Directory Read Errors', () => {
255
- test('BUG: Errore lettura directory dovrebbe essere gestito', async () => {
256
- const app = new Koa();
257
-
258
- // Crea una directory temporanea
259
- const tempDir = path.join(rootDir, 'temp-test-dir');
260
- if (!fs.existsSync(tempDir)) {
261
- fs.mkdirSync(tempDir);
262
- }
228
+ test('FIXED: Unreadable directory returns 500, server does not crash', async () => {
229
+ // Skip on Windows (chmod has no effect) and when running as root
230
+ // (root ignores permission bits, so chmod 0o000 has no effect).
231
+ if (process.platform === 'win32') return;
232
+ if (typeof process.getuid === 'function' && process.getuid() === 0) return;
263
233
 
264
- app.use(koaClassicServer(rootDir, {
265
- showDirContents: true
266
- }));
234
+ const app = new Koa();
235
+ const tempDir = path.join(rootDir, 'temp-perm-test-dir');
236
+ if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir);
267
237
 
238
+ app.use(koaClassicServer(rootDir, { dirListing: { enabled: true } }));
268
239
  const server = app.listen();
269
240
 
270
241
  try {
271
- // Prima richiesta normale
272
- const res1 = await supertest(server).get('/temp-test-dir');
242
+ const res1 = await supertest(server).get('/temp-perm-test-dir');
273
243
  expect(res1.status).toBe(200);
274
244
 
275
- // Ora cambia i permessi (solo su Unix)
276
- if (process.platform !== 'win32') {
277
- fs.chmodSync(tempDir, 0o000); // Nessun permesso
278
-
279
- const res2 = await supertest(server).get('/temp-test-dir');
280
-
281
- console.log('🐛 Directory Permission Test - Status:', res2.status);
282
-
283
- // Dovrebbe gestire l'errore
284
- // expect(res2.status).toBe(500) o 403;
285
- }
286
- } catch (error) {
287
- console.log('⚠️ Directory read error:', error.message);
245
+ fs.chmodSync(tempDir, 0o000);
246
+ const res2 = await supertest(server).get('/temp-perm-test-dir');
247
+ expect([403, 500]).toContain(res2.status);
288
248
  } finally {
289
- // Ripristina permessi e cleanup
290
- if (process.platform !== 'win32' && fs.existsSync(tempDir)) {
291
- try {
292
- fs.chmodSync(tempDir, 0o755);
293
- } catch (e) {}
294
- }
295
- if (fs.existsSync(tempDir)) {
296
- fs.rmdirSync(tempDir);
297
- }
249
+ try { fs.chmodSync(tempDir, 0o755); } catch { /* ignore */ }
250
+ if (fs.existsSync(tempDir)) fs.rmdirSync(tempDir);
298
251
  server.close();
299
252
  }
300
253
  });
301
254
  });
302
255
 
256
+ // ─── Content-Disposition ──────────────────────────────────────────────────────
257
+
303
258
  describe('Bug Tests - Content-Disposition', () => {
304
259
  let app;
305
260
  let server;
@@ -310,27 +265,58 @@ describe('Bug Tests - Content-Disposition', () => {
310
265
  server = app.listen();
311
266
  });
312
267
 
313
- test('BUG: Filename con caratteri speciali dovrebbe essere quotato', async () => {
314
- // Crea file con spazi e caratteri speciali
268
+ afterAll(() => {
269
+ server.close();
270
+ });
271
+
272
+ test('FIXED: Filename with spaces is quoted in Content-Disposition', async () => {
273
+ const testFile = path.join(rootDir, 'file with spaces.txt');
274
+ fs.writeFileSync(testFile, 'test content');
275
+
276
+ try {
277
+ const res = await supertest(server).get('/file%20with%20spaces.txt');
278
+ const contentDisp = res.headers['content-disposition'];
279
+ // quoted-string form must wrap the filename in double quotes
280
+ expect(contentDisp).toMatch(/filename="file with spaces\.txt"/);
281
+ } finally {
282
+ if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
283
+ }
284
+ });
285
+
286
+ test('FIXED: Filename with special chars has RFC 5987 extended form', async () => {
315
287
  const testFile = path.join(rootDir, 'file with spaces & special.txt');
316
288
  fs.writeFileSync(testFile, 'test content');
317
289
 
318
290
  try {
319
291
  const res = await supertest(server).get('/file%20with%20spaces%20%26%20special.txt');
320
-
321
292
  const contentDisp = res.headers['content-disposition'];
322
- console.log('🐛 Content-Disposition:', contentDisp);
323
-
324
- // Dovrebbe essere quotato
325
- // expect(contentDisp).toMatch(/"file with spaces & special.txt"/);
293
+ // RFC 5987 extended form must be present with UTF-8 encoding prefix
294
+ expect(contentDisp).toMatch(/filename\*=UTF-8''/);
295
+ // The & must be percent-encoded as %26 in the extended form
296
+ expect(contentDisp).toContain('%26');
326
297
  } finally {
327
- if (fs.existsSync(testFile)) {
328
- fs.unlinkSync(testFile);
329
- }
298
+ if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
330
299
  }
331
300
  });
332
301
 
333
- afterAll(() => {
334
- server.close();
302
+ test('FIXED: Filename with double-quote is safely escaped', async () => {
303
+ // Create file with a double-quote in its name (valid on Linux)
304
+ let testFile;
305
+ try {
306
+ testFile = path.join(rootDir, 'file"name.txt');
307
+ fs.writeFileSync(testFile, 'test');
308
+ } catch {
309
+ // Some filesystems disallow " in filenames — skip gracefully
310
+ return;
311
+ }
312
+
313
+ try {
314
+ const res = await supertest(server).get('/file%22name.txt');
315
+ const contentDisp = res.headers['content-disposition'];
316
+ // The " inside the quoted-string must be escaped as \"
317
+ expect(contentDisp).toMatch(/filename="file\\"name\.txt"/);
318
+ } finally {
319
+ if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
320
+ }
335
321
  });
336
322
  });
@@ -0,0 +1 @@
1
+ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
@@ -0,0 +1 @@
1
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA