first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,497 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/CompileController'
)
const Errors = require('../../../app/js/Errors')
describe('CompileController', function () {
beforeEach(function () {
this.buildId = 'build-id-123'
this.CompileController = SandboxedModule.require(modulePath, {
requires: {
'./CompileManager': (this.CompileManager = {}),
'./RequestParser': (this.RequestParser = {}),
'@overleaf/settings': (this.Settings = {
apis: {
clsi: {
url: 'http://clsi.example.com',
outputUrlPrefix: '/zone/b',
downloadHost: 'http://localhost:3013',
},
clsiCache: {
enabled: false,
url: 'http://localhost:3044',
},
},
}),
'@overleaf/metrics': {
Timer: sinon.stub().returns({ done: sinon.stub() }),
},
'./ProjectPersistenceManager': (this.ProjectPersistenceManager = {}),
'./CLSICacheHandler': {
notifyCLSICacheAboutBuild: sinon.stub(),
downloadLatestCompileCache: sinon.stub().resolves(),
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
},
'./Errors': (this.Erros = Errors),
},
})
this.Settings.externalUrl = 'http://www.example.com'
this.req = {}
this.res = {}
this.next = sinon.stub()
})
describe('compile', function () {
beforeEach(function () {
this.req.body = {
compile: 'mock-body',
}
this.req.params = { project_id: (this.project_id = 'project-id-123') }
this.request = {
compile: 'mock-parsed-request',
}
this.request_with_project_id = {
compile: this.request.compile,
project_id: this.project_id,
}
this.output_files = [
{
path: 'output.pdf',
type: 'pdf',
size: 1337,
build: 1234,
},
{
path: 'output.log',
type: 'log',
build: 1234,
},
]
this.RequestParser.parse = sinon
.stub()
.callsArgWith(1, null, this.request)
this.ProjectPersistenceManager.markProjectAsJustAccessed = sinon
.stub()
.callsArg(1)
this.stats = { foo: 1 }
this.timings = { bar: 2 }
this.res.status = sinon.stub().returnsThis()
this.res.send = sinon.stub()
this.CompileManager.doCompileWithLock = sinon
.stub()
.callsFake((_req, stats, timings, cb) => {
Object.assign(stats, this.stats)
Object.assign(timings, this.timings)
cb(null, {
outputFiles: this.output_files,
buildId: this.buildId,
})
})
})
describe('successfully', function () {
beforeEach(function () {
this.CompileController.compile(this.req, this.res)
})
it('should parse the request', function () {
this.RequestParser.parse.calledWith(this.req.body).should.equal(true)
})
it('should run the compile for the specified project', function () {
this.CompileManager.doCompileWithLock
.calledWith(this.request_with_project_id)
.should.equal(true)
})
it('should mark the project as accessed', function () {
this.ProjectPersistenceManager.markProjectAsJustAccessed
.calledWith(this.project_id)
.should.equal(true)
})
it('should return the JSON response', function () {
this.res.status.calledWith(200).should.equal(true)
this.res.send
.calledWith({
compile: {
status: 'success',
error: null,
stats: this.stats,
timings: this.timings,
buildId: this.buildId,
outputUrlPrefix: '/zone/b',
outputFiles: this.output_files.map(file => ({
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
...file,
})),
},
})
.should.equal(true)
})
})
describe('without a outputUrlPrefix', function () {
beforeEach(function () {
this.Settings.apis.clsi.outputUrlPrefix = ''
this.CompileController.compile(this.req, this.res)
})
it('should return the JSON response with empty outputUrlPrefix', function () {
this.res.status.calledWith(200).should.equal(true)
this.res.send
.calledWith({
compile: {
status: 'success',
error: null,
stats: this.stats,
timings: this.timings,
buildId: this.buildId,
outputUrlPrefix: '',
outputFiles: this.output_files.map(file => ({
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
...file,
})),
},
})
.should.equal(true)
})
})
describe('with user provided fake_output.pdf', function () {
beforeEach(function () {
this.output_files = [
{
path: 'fake_output.pdf',
type: 'pdf',
build: 1234,
},
{
path: 'output.log',
type: 'log',
build: 1234,
},
]
this.CompileManager.doCompileWithLock = sinon
.stub()
.callsFake((_req, stats, timings, cb) => {
Object.assign(stats, this.stats)
Object.assign(timings, this.timings)
cb(null, {
outputFiles: this.output_files,
buildId: this.buildId,
})
})
this.CompileController.compile(this.req, this.res)
})
it('should return the JSON response with status failure', function () {
this.res.status.calledWith(200).should.equal(true)
this.res.send.should.have.been.calledWith({
compile: {
status: 'failure',
error: null,
stats: this.stats,
timings: this.timings,
outputUrlPrefix: '/zone/b',
buildId: this.buildId,
outputFiles: this.output_files.map(file => ({
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
...file,
})),
},
})
})
})
describe('with an empty output.pdf', function () {
beforeEach(function () {
this.output_files = [
{
path: 'output.pdf',
type: 'pdf',
size: 0,
build: 1234,
},
{
path: 'output.log',
type: 'log',
build: 1234,
},
]
this.CompileManager.doCompileWithLock = sinon
.stub()
.callsFake((_req, stats, timings, cb) => {
Object.assign(stats, this.stats)
Object.assign(timings, this.timings)
cb(null, {
outputFiles: this.output_files,
buildId: this.buildId,
})
})
this.CompileController.compile(this.req, this.res)
})
it('should return the JSON response with status failure', function () {
this.res.status.calledWith(200).should.equal(true)
this.res.send.should.have.been.calledWith({
compile: {
status: 'failure',
error: null,
stats: this.stats,
buildId: this.buildId,
timings: this.timings,
outputUrlPrefix: '/zone/b',
outputFiles: this.output_files.map(file => ({
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
...file,
})),
},
})
})
})
describe('with an error', function () {
beforeEach(function () {
const error = new Error((this.message = 'error message'))
error.buildId = this.buildId
this.CompileManager.doCompileWithLock = sinon
.stub()
.callsFake((_req, stats, timings, cb) => {
Object.assign(stats, this.stats)
Object.assign(timings, this.timings)
cb(error)
})
this.CompileController.compile(this.req, this.res)
})
it('should return the JSON response with the error', function () {
this.res.status.calledWith(500).should.equal(true)
this.res.send
.calledWith({
compile: {
status: 'error',
error: this.message,
outputUrlPrefix: '/zone/b',
outputFiles: [],
buildId: this.buildId,
stats: this.stats,
timings: this.timings,
},
})
.should.equal(true)
})
})
describe('with too many compile requests error', function () {
beforeEach(function () {
const error = new Errors.TooManyCompileRequestsError(
'too many concurrent compile requests'
)
this.CompileManager.doCompileWithLock = sinon
.stub()
.callsFake((_req, stats, timings, cb) => {
Object.assign(stats, this.stats)
Object.assign(timings, this.timings)
cb(error)
})
this.CompileController.compile(this.req, this.res)
})
it('should return the JSON response with the error', function () {
this.res.status.calledWith(503).should.equal(true)
this.res.send
.calledWith({
compile: {
status: 'unavailable',
error: 'too many concurrent compile requests',
outputUrlPrefix: '/zone/b',
outputFiles: [],
stats: this.stats,
timings: this.timings,
// JSON.stringify will omit these undefined values
buildId: undefined,
},
})
.should.equal(true)
})
})
describe('when the request times out', function () {
beforeEach(function () {
this.error = new Error((this.message = 'container timed out'))
this.error.timedout = true
this.CompileManager.doCompileWithLock = sinon
.stub()
.callsFake((_req, stats, timings, cb) => {
Object.assign(stats, this.stats)
Object.assign(timings, this.timings)
cb(this.error)
})
this.CompileController.compile(this.req, this.res)
})
it('should return the JSON response with the timeout status', function () {
this.res.status.calledWith(200).should.equal(true)
this.res.send
.calledWith({
compile: {
status: 'timedout',
error: this.message,
outputUrlPrefix: '/zone/b',
outputFiles: [],
stats: this.stats,
timings: this.timings,
// JSON.stringify will omit these undefined values
buildId: undefined,
},
})
.should.equal(true)
})
})
describe('when the request returns no output files', function () {
beforeEach(function () {
this.CompileManager.doCompileWithLock = sinon
.stub()
.callsFake((_req, stats, timings, cb) => {
Object.assign(stats, this.stats)
Object.assign(timings, this.timings)
cb(null, {})
})
this.CompileController.compile(this.req, this.res)
})
it('should return the JSON response with the failure status', function () {
this.res.status.calledWith(200).should.equal(true)
this.res.send
.calledWith({
compile: {
error: null,
status: 'failure',
outputUrlPrefix: '/zone/b',
outputFiles: [],
stats: this.stats,
timings: this.timings,
// JSON.stringify will omit these undefined values
buildId: undefined,
},
})
.should.equal(true)
})
})
})
describe('syncFromCode', function () {
beforeEach(function () {
this.file = 'main.tex'
this.line = 42
this.column = 5
this.project_id = 'mock-project-id'
this.req.params = { project_id: this.project_id }
this.req.query = {
file: this.file,
line: this.line.toString(),
column: this.column.toString(),
}
this.res.json = sinon.stub()
this.CompileManager.syncFromCode = sinon
.stub()
.yields(null, (this.pdfPositions = ['mock-positions']))
this.CompileController.syncFromCode(this.req, this.res, this.next)
})
it('should find the corresponding location in the PDF', function () {
this.CompileManager.syncFromCode
.calledWith(
this.project_id,
undefined,
this.file,
this.line,
this.column
)
.should.equal(true)
})
it('should return the positions', function () {
this.res.json
.calledWith({
pdf: this.pdfPositions,
})
.should.equal(true)
})
})
describe('syncFromPdf', function () {
beforeEach(function () {
this.page = 5
this.h = 100.23
this.v = 45.67
this.project_id = 'mock-project-id'
this.req.params = { project_id: this.project_id }
this.req.query = {
page: this.page.toString(),
h: this.h.toString(),
v: this.v.toString(),
}
this.res.json = sinon.stub()
this.CompileManager.syncFromPdf = sinon
.stub()
.yields(null, (this.codePositions = ['mock-positions']))
this.CompileController.syncFromPdf(this.req, this.res, this.next)
})
it('should find the corresponding location in the code', function () {
this.CompileManager.syncFromPdf
.calledWith(this.project_id, undefined, this.page, this.h, this.v)
.should.equal(true)
})
it('should return the positions', function () {
this.res.json
.calledWith({
code: this.codePositions,
})
.should.equal(true)
})
})
describe('wordcount', function () {
beforeEach(function () {
this.file = 'main.tex'
this.project_id = 'mock-project-id'
this.req.params = { project_id: this.project_id }
this.req.query = {
file: this.file,
image: (this.image = 'example.com/image'),
}
this.res.json = sinon.stub()
this.CompileManager.wordcount = sinon
.stub()
.callsArgWith(4, null, (this.texcount = ['mock-texcount']))
})
it('should return the word count of a file', function () {
this.CompileController.wordcount(this.req, this.res, this.next)
this.CompileManager.wordcount
.calledWith(this.project_id, undefined, this.file, this.image)
.should.equal(true)
})
it('should return the texcount info', function () {
this.CompileController.wordcount(this.req, this.res, this.next)
this.res.json
.calledWith({
texcount: this.texcount,
})
.should.equal(true)
})
})
})

View File

@@ -0,0 +1,627 @@
const Path = require('node:path')
const SandboxedModule = require('sandboxed-module')
const { expect } = require('chai')
const sinon = require('sinon')
const MODULE_PATH = require('node:path').join(
__dirname,
'../../../app/js/CompileManager'
)
describe('CompileManager', function () {
beforeEach(function () {
this.projectId = 'project-id-123'
this.userId = '1234'
this.resources = 'mock-resources'
this.outputFiles = [
{
path: 'output.log',
type: 'log',
},
{
path: 'output.pdf',
type: 'pdf',
},
]
this.buildFiles = [
{
path: 'output.log',
type: 'log',
build: 1234,
},
{
path: 'output.pdf',
type: 'pdf',
build: 1234,
},
]
this.buildId = 'build-id-123'
this.commandOutput = 'Dummy output'
this.compileBaseDir = '/compile/dir'
this.outputBaseDir = '/output/dir'
this.compileDir = `${this.compileBaseDir}/${this.projectId}-${this.userId}`
this.outputDir = `${this.outputBaseDir}/${this.projectId}-${this.userId}`
this.LatexRunner = {
promises: {
runLatex: sinon.stub().resolves({}),
},
}
this.ResourceWriter = {
promises: {
syncResourcesToDisk: sinon.stub().resolves(this.resources),
},
}
this.OutputFileFinder = {
promises: {
findOutputFiles: sinon.stub().resolves({
outputFiles: this.outputFiles,
allEntries: this.outputFiles.map(f => f.path).concat(['main.tex']),
}),
},
}
this.OutputCacheManager = {
promises: {
queueDirOperation: sinon.stub().callsArg(1),
saveOutputFiles: sinon
.stub()
.resolves({ outputFiles: this.buildFiles, buildId: this.buildId }),
},
}
this.Settings = {
path: {
compilesDir: this.compileBaseDir,
outputDir: this.outputBaseDir,
synctexBaseDir: sinon.stub(),
},
clsi: {
docker: {
image: 'SOMEIMAGE',
},
},
}
this.Settings.path.synctexBaseDir
.withArgs(`${this.projectId}-${this.userId}`)
.returns(this.compileDir)
this.child_process = {
exec: sinon.stub(),
execFile: sinon.stub().yields(),
}
this.CommandRunner = {
promises: {
run: sinon.stub().callsFake((_1, _2, _3, _4, _5, _6, compileGroup) => {
if (compileGroup === 'synctex') {
return Promise.resolve({ stdout: this.commandOutput })
} else {
return Promise.resolve({
stdout: 'Encoding: ascii\nWords in text: 2',
})
}
}),
},
}
this.DraftModeManager = {
promises: {
injectDraftMode: sinon.stub().resolves(),
},
}
this.TikzManager = {
promises: {
checkMainFile: sinon.stub().resolves(false),
},
}
this.lock = {
release: sinon.stub(),
}
this.LockManager = {
acquire: sinon.stub().returns(this.lock),
}
this.SynctexOutputParser = {
parseViewOutput: sinon.stub(),
parseEditOutput: sinon.stub(),
}
this.dirStats = {
isDirectory: sinon.stub().returns(true),
}
this.fileStats = {
isFile: sinon.stub().returns(true),
}
this.fsPromises = {
lstat: sinon.stub(),
stat: sinon.stub(),
readFile: sinon.stub(),
mkdir: sinon.stub().resolves(),
rm: sinon.stub().resolves(),
unlink: sinon.stub().resolves(),
rmdir: sinon.stub().resolves(),
}
this.fsPromises.lstat.withArgs(this.compileDir).resolves(this.dirStats)
this.fsPromises.stat
.withArgs(Path.join(this.compileDir, 'output.synctex.gz'))
.resolves(this.fileStats)
this.CompileManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'./LatexRunner': this.LatexRunner,
'./ResourceWriter': this.ResourceWriter,
'./OutputFileFinder': this.OutputFileFinder,
'./OutputCacheManager': this.OutputCacheManager,
'@overleaf/settings': this.Settings,
'@overleaf/metrics': {
inc: sinon.stub(),
timing: sinon.stub(),
gauge: sinon.stub(),
Timer: sinon.stub().returns({ done: sinon.stub() }),
},
child_process: this.child_process,
'./CommandRunner': this.CommandRunner,
'./DraftModeManager': this.DraftModeManager,
'./TikzManager': this.TikzManager,
'./LockManager': this.LockManager,
'./SynctexOutputParser': this.SynctexOutputParser,
'fs/promises': this.fsPromises,
'./CLSICacheHandler': {
notifyCLSICacheAboutBuild: sinon.stub(),
downloadLatestCompileCache: sinon.stub().resolves(),
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
},
},
})
})
describe('doCompileWithLock', function () {
beforeEach(function () {
this.request = {
resources: this.resources,
rootResourcePath: (this.rootResourcePath = 'main.tex'),
project_id: this.projectId,
user_id: this.userId,
compiler: (this.compiler = 'pdflatex'),
timeout: (this.timeout = 42000),
imageName: (this.image = 'example.com/image'),
flags: (this.flags = ['-file-line-error']),
compileGroup: (this.compileGroup = 'compile-group'),
stopOnFirstError: false,
metricsOpts: {
path: 'clsi-perf',
method: 'minimal',
compile: 'initial',
},
}
this.env = {
OVERLEAF_PROJECT_ID: this.projectId,
}
})
describe('when the project is locked', function () {
beforeEach(async function () {
const error = new Error('locked')
this.LockManager.acquire.throws(error)
await expect(
this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
).to.be.rejectedWith(error)
})
it('should ensure that the compile directory exists', function () {
expect(this.fsPromises.mkdir).to.have.been.calledWith(this.compileDir, {
recursive: true,
})
})
it('should not run LaTeX', function () {
expect(this.LatexRunner.promises.runLatex).not.to.have.been.called
})
})
describe('normally', function () {
beforeEach(async function () {
this.result = await this.CompileManager.promises.doCompileWithLock(
this.request,
{},
{}
)
})
it('should ensure that the compile directory exists', function () {
expect(this.fsPromises.mkdir).to.have.been.calledWith(this.compileDir, {
recursive: true,
})
})
it('should write the resources to disk', function () {
expect(
this.ResourceWriter.promises.syncResourcesToDisk
).to.have.been.calledWith(this.request, this.compileDir)
})
it('should run LaTeX', function () {
expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
`${this.projectId}-${this.userId}`,
{
directory: this.compileDir,
mainFile: this.rootResourcePath,
compiler: this.compiler,
timeout: this.timeout,
image: this.image,
flags: this.flags,
environment: this.env,
compileGroup: this.compileGroup,
stopOnFirstError: this.request.stopOnFirstError,
stats: sinon.match.object,
timings: sinon.match.object,
}
)
})
it('should find the output files', function () {
expect(
this.OutputFileFinder.promises.findOutputFiles
).to.have.been.calledWith(this.resources, this.compileDir)
})
it('should return the output files', function () {
expect(this.result.outputFiles).to.equal(this.buildFiles)
})
it('should not inject draft mode by default', function () {
expect(this.DraftModeManager.promises.injectDraftMode).not.to.have.been
.called
})
})
describe('with draft mode', function () {
beforeEach(async function () {
this.request.draft = true
await this.CompileManager.promises.doCompileWithLock(
this.request,
{},
{}
)
})
it('should inject the draft mode header', function () {
expect(
this.DraftModeManager.promises.injectDraftMode
).to.have.been.calledWith(this.compileDir + '/' + this.rootResourcePath)
})
})
describe('with a check option', function () {
beforeEach(async function () {
this.request.check = 'error'
await this.CompileManager.promises.doCompileWithLock(
this.request,
{},
{}
)
})
it('should run chktex', function () {
expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
`${this.projectId}-${this.userId}`,
{
directory: this.compileDir,
mainFile: this.rootResourcePath,
compiler: this.compiler,
timeout: this.timeout,
image: this.image,
flags: this.flags,
environment: {
CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16',
CHKTEX_EXIT_ON_ERROR: 1,
CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000',
OVERLEAF_PROJECT_ID: this.projectId,
},
compileGroup: this.compileGroup,
stopOnFirstError: this.request.stopOnFirstError,
stats: sinon.match.object,
timings: sinon.match.object,
}
)
})
})
describe('with a knitr file and check options', function () {
beforeEach(async function () {
this.request.rootResourcePath = 'main.Rtex'
this.request.check = 'error'
await this.CompileManager.promises.doCompileWithLock(
this.request,
{},
{}
)
})
it('should not run chktex', function () {
expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
`${this.projectId}-${this.userId}`,
{
directory: this.compileDir,
mainFile: 'main.Rtex',
compiler: this.compiler,
timeout: this.timeout,
image: this.image,
flags: this.flags,
environment: this.env,
compileGroup: this.compileGroup,
stopOnFirstError: this.request.stopOnFirstError,
stats: sinon.match.object,
timings: sinon.match.object,
}
)
})
})
describe('when the compile times out', function () {
beforeEach(async function () {
const error = new Error('timed out!')
error.timedout = true
this.LatexRunner.promises.runLatex.rejects(error)
await expect(
this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
).to.be.rejected
})
it('should clear the compile directory', function () {
for (const { path } of this.buildFiles) {
expect(this.fsPromises.unlink).to.have.been.calledWith(
this.compileDir + '/' + path
)
}
expect(this.fsPromises.unlink).to.have.been.calledWith(
this.compileDir + '/main.tex'
)
expect(this.fsPromises.rmdir).to.have.been.calledWith(this.compileDir)
})
})
describe('when the compile is manually stopped', function () {
beforeEach(async function () {
const error = new Error('terminated!')
error.terminated = true
this.LatexRunner.promises.runLatex.rejects(error)
await expect(
this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
).to.be.rejected
})
it('should clear the compile directory', function () {
for (const { path } of this.buildFiles) {
expect(this.fsPromises.unlink).to.have.been.calledWith(
this.compileDir + '/' + path
)
}
expect(this.fsPromises.unlink).to.have.been.calledWith(
this.compileDir + '/main.tex'
)
expect(this.fsPromises.rmdir).to.have.been.calledWith(this.compileDir)
})
})
})
describe('clearProject', function () {
it('should clear the compile directory', async function () {
await this.CompileManager.promises.clearProject(
this.projectId,
this.userId
)
expect(this.fsPromises.rm).to.have.been.calledWith(this.compileDir, {
force: true,
recursive: true,
})
})
})
describe('syncing', function () {
beforeEach(function () {
this.page = 1
this.h = 42.23
this.v = 87.56
this.width = 100.01
this.height = 234.56
this.line = 5
this.column = 3
this.filename = 'main.tex'
})
describe('syncFromCode', function () {
beforeEach(function () {
this.records = [{ page: 1, h: 2, v: 3, width: 4, height: 5 }]
this.SynctexOutputParser.parseViewOutput
.withArgs(this.commandOutput)
.returns(this.records)
})
describe('normal case', function () {
beforeEach(async function () {
this.result = await this.CompileManager.promises.syncFromCode(
this.projectId,
this.userId,
this.filename,
this.line,
this.column,
''
)
})
it('should execute the synctex binary', function () {
const outputFilePath = `${this.compileDir}/output.pdf`
const inputFilePath = `${this.compileDir}/${this.filename}`
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
`${this.projectId}-${this.userId}`,
[
'synctex',
'view',
'-i',
`${this.line}:${this.column}:${inputFilePath}`,
'-o',
outputFilePath,
],
this.compileDir,
this.Settings.clsi.docker.image,
60000,
{}
)
})
it('should return the parsed output', function () {
expect(this.result).to.deep.equal(this.records)
})
})
describe('with a custom imageName', function () {
const customImageName = 'foo/bar:tag-0'
beforeEach(async function () {
await this.CompileManager.promises.syncFromCode(
this.projectId,
this.userId,
this.filename,
this.line,
this.column,
{ imageName: customImageName }
)
})
it('should execute the synctex binary in a custom docker image', function () {
const outputFilePath = `${this.compileDir}/output.pdf`
const inputFilePath = `${this.compileDir}/${this.filename}`
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
`${this.projectId}-${this.userId}`,
[
'synctex',
'view',
'-i',
`${this.line}:${this.column}:${inputFilePath}`,
'-o',
outputFilePath,
],
this.compileDir,
customImageName,
60000,
{}
)
})
})
})
describe('syncFromPdf', function () {
beforeEach(function () {
this.records = [{ file: 'main.tex', line: 1, column: 1 }]
this.SynctexOutputParser.parseEditOutput
.withArgs(this.commandOutput, this.compileDir)
.returns(this.records)
})
describe('normal case', function () {
beforeEach(async function () {
this.result = await this.CompileManager.promises.syncFromPdf(
this.projectId,
this.userId,
this.page,
this.h,
this.v,
{ imageName: '' }
)
})
it('should execute the synctex binary', function () {
const outputFilePath = `${this.compileDir}/output.pdf`
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
`${this.projectId}-${this.userId}`,
[
'synctex',
'edit',
'-o',
`${this.page}:${this.h}:${this.v}:${outputFilePath}`,
],
this.compileDir,
this.Settings.clsi.docker.image,
60000,
{}
)
})
it('should return the parsed output', function () {
expect(this.result).to.deep.equal(this.records)
})
})
describe('with a custom imageName', function () {
const customImageName = 'foo/bar:tag-1'
beforeEach(async function () {
await this.CompileManager.promises.syncFromPdf(
this.projectId,
this.userId,
this.page,
this.h,
this.v,
{ imageName: customImageName }
)
})
it('should execute the synctex binary in a custom docker image', function () {
const outputFilePath = `${this.compileDir}/output.pdf`
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
`${this.projectId}-${this.userId}`,
[
'synctex',
'edit',
'-o',
`${this.page}:${this.h}:${this.v}:${outputFilePath}`,
],
this.compileDir,
customImageName,
60000,
{}
)
})
})
})
})
describe('wordcount', function () {
beforeEach(async function () {
this.timeout = 60 * 1000
this.filename = 'main.tex'
this.image = 'example.com/image'
this.result = await this.CompileManager.promises.wordcount(
this.projectId,
this.userId,
this.filename,
this.image
)
})
it('should run the texcount command', function () {
this.filePath = `$COMPILE_DIR/${this.filename}`
this.command = ['texcount', '-nocol', '-inc', this.filePath]
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
`${this.projectId}-${this.userId}`,
this.command,
this.compileDir,
this.image,
this.timeout,
{}
)
})
it('should return the parsed output', function () {
expect(this.result).to.deep.equal({
encode: 'ascii',
textWords: 2,
headWords: 0,
outside: 0,
headers: 0,
elements: 0,
mathInline: 0,
mathDisplay: 0,
errors: 0,
messages: '',
})
})
})
})

View File

@@ -0,0 +1,223 @@
const fs = require('node:fs')
const Path = require('node:path')
const { expect } = require('chai')
const MODULE_PATH = '../../../app/js/ContentCacheManager'
describe('ContentCacheManager', function () {
let contentDir, pdfPath, xrefPath
let ContentCacheManager, files, Settings
before(function () {
Settings = require('@overleaf/settings')
ContentCacheManager = require(MODULE_PATH)
})
let contentRanges, newContentRanges, reclaimed
async function run(filePath, pdfSize, pdfCachingMinChunkSize) {
const result = await ContentCacheManager.promises.update({
contentDir,
filePath,
pdfSize,
pdfCachingMinChunkSize,
compileTime: 1337,
})
let newlyReclaimed
;({
contentRanges,
newContentRanges,
reclaimedSpace: newlyReclaimed,
} = result)
reclaimed += newlyReclaimed
const fileNames = await fs.promises.readdir(contentDir)
files = {}
for (const fileName of fileNames) {
const path = Path.join(contentDir, fileName)
files[path] = await fs.promises.readFile(path)
}
}
before(function () {
contentDir =
'/overleaf/services/clsi/output/602cee6f6460fca0ba7921e6/content/1797a7f48f9-5abc1998509dea1f'
pdfPath =
'/overleaf/services/clsi/output/602cee6f6460fca0ba7921e6/generated-files/1797a7f48ea-8ac6805139f43351/output.pdf'
xrefPath =
'/overleaf/services/clsi/output/602cee6f6460fca0ba7921e6/generated-files/1797a7f48ea-8ac6805139f43351/output.pdfxref'
reclaimed = 0
Settings.pdfCachingMinChunkSize = 1024
})
before(async function () {
await fs.promises.rm(contentDir, { recursive: true, force: true })
await fs.promises.mkdir(contentDir, { recursive: true })
await fs.promises.mkdir(Path.dirname(pdfPath), { recursive: true })
})
describe('minimal', function () {
const PATH_MINIMAL_PDF = 'test/acceptance/fixtures/minimal.pdf'
const PATH_MINIMAL_XREF = 'test/acceptance/fixtures/minimal.pdfxref'
const OBJECT_ID_1 = '9 0 '
const HASH_LARGE =
'd7cfc73ad2fba4578a437517923e3714927bbf35e63ea88bd93c7a8076cf1fcd'
const OBJECT_ID_2 = '10 0 '
const HASH_SMALL =
'896749b8343851b0dc385f71616916a7ba0434fcfb56d1fc7e27cd139eaa2f71'
function getChunkPath(hash) {
return Path.join('test/unit/js/snapshots/minimalCompile/chunks', hash)
}
let MINIMAL_SIZE, RANGE_1, RANGE_2, h1, h2, START_1, START_2, END_1, END_2
before(async function () {
await fs.promises.copyFile(PATH_MINIMAL_PDF, pdfPath)
await fs.promises.copyFile(PATH_MINIMAL_XREF, xrefPath)
const MINIMAL = await fs.promises.readFile(PATH_MINIMAL_PDF)
MINIMAL_SIZE = (await fs.promises.stat(PATH_MINIMAL_PDF)).size
RANGE_1 = await fs.promises.readFile(getChunkPath(HASH_LARGE))
RANGE_2 = await fs.promises.readFile(getChunkPath(HASH_SMALL))
h1 = HASH_LARGE
h2 = HASH_SMALL
START_1 = MINIMAL.indexOf(RANGE_1)
END_1 = START_1 + RANGE_1.byteLength
START_2 = MINIMAL.indexOf(RANGE_2)
END_2 = START_2 + RANGE_2.byteLength
})
async function runWithMinimal(pdfCachingMinChunkSize) {
await run(pdfPath, MINIMAL_SIZE, pdfCachingMinChunkSize)
}
describe('with two ranges qualifying', function () {
before(async function () {
await runWithMinimal(500)
})
it('should produce two ranges', function () {
expect(contentRanges).to.have.length(2)
})
it('should find the correct offsets', function () {
expect(contentRanges).to.deep.equal([
{
objectId: OBJECT_ID_1,
start: START_1,
end: END_1,
hash: h1,
},
{
objectId: OBJECT_ID_2,
start: START_2,
end: END_2,
hash: h2,
},
])
})
it('should store the contents', function () {
expect(files).to.deep.equal({
[Path.join(contentDir, h1)]: RANGE_1,
[Path.join(contentDir, h2)]: RANGE_2,
[Path.join(contentDir, '.state.v0.json')]: Buffer.from(
JSON.stringify({
hashAge: [
[h1, 0],
[h2, 0],
],
hashSize: [
[h1, RANGE_1.byteLength],
[h2, RANGE_2.byteLength],
],
})
),
})
})
it('should mark all ranges as new', function () {
expect(contentRanges).to.deep.equal(newContentRanges)
})
describe('when re-running with one range too small', function () {
before(async function () {
await runWithMinimal(1024)
})
it('should produce one range', function () {
expect(contentRanges).to.have.length(1)
})
it('should find the correct offsets', function () {
expect(contentRanges).to.deep.equal([
{
objectId: OBJECT_ID_1,
start: START_1,
end: END_1,
hash: h1,
},
])
})
it('should update the age of the 2nd range', function () {
expect(files).to.deep.equal({
[Path.join(contentDir, h1)]: RANGE_1,
[Path.join(contentDir, h2)]: RANGE_2,
[Path.join(contentDir, '.state.v0.json')]: Buffer.from(
JSON.stringify({
hashAge: [
[h1, 0],
[h2, 1],
],
hashSize: [
[h1, RANGE_1.byteLength],
[h2, RANGE_2.byteLength],
],
})
),
})
})
it('should find no new ranges', function () {
expect(newContentRanges).to.deep.equal([])
})
describe('when re-running 5 more times', function () {
for (let i = 0; i < 5; i++) {
before(async function () {
await runWithMinimal(1024)
})
}
it('should still produce one range', function () {
expect(contentRanges).to.have.length(1)
})
it('should still find the correct offsets', function () {
expect(contentRanges).to.deep.equal([
{
objectId: OBJECT_ID_1,
start: START_1,
end: END_1,
hash: h1,
},
])
})
it('should delete the 2nd range', function () {
expect(files).to.deep.equal({
[Path.join(contentDir, h1)]: RANGE_1,
[Path.join(contentDir, '.state.v0.json')]: Buffer.from(
JSON.stringify({
hashAge: [[h1, 0]],
hashSize: [[h1, RANGE_1.byteLength]],
})
),
})
})
it('should find no new ranges', function () {
expect(newContentRanges).to.deep.equal([])
})
it('should yield the reclaimed space', function () {
expect(reclaimed).to.equal(RANGE_2.byteLength)
})
})
})
})
})
})

View File

@@ -0,0 +1,79 @@
/* eslint-disable
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/ContentTypeMapper'
)
describe('ContentTypeMapper', function () {
beforeEach(function () {
return (this.ContentTypeMapper = SandboxedModule.require(modulePath))
})
return describe('map', function () {
it('should map .txt to text/plain', function () {
const contentType = this.ContentTypeMapper.map('example.txt')
return contentType.should.equal('text/plain')
})
it('should map .csv to text/csv', function () {
const contentType = this.ContentTypeMapper.map('example.csv')
return contentType.should.equal('text/csv')
})
it('should map .pdf to application/pdf', function () {
const contentType = this.ContentTypeMapper.map('example.pdf')
return contentType.should.equal('application/pdf')
})
it('should fall back to octet-stream', function () {
const contentType = this.ContentTypeMapper.map('example.unknown')
return contentType.should.equal('application/octet-stream')
})
describe('coercing web files to plain text', function () {
it('should map .js to plain text', function () {
const contentType = this.ContentTypeMapper.map('example.js')
return contentType.should.equal('text/plain')
})
it('should map .html to plain text', function () {
const contentType = this.ContentTypeMapper.map('example.html')
return contentType.should.equal('text/plain')
})
return it('should map .css to plain text', function () {
const contentType = this.ContentTypeMapper.map('example.css')
return contentType.should.equal('text/plain')
})
})
return describe('image files', function () {
it('should map .png to image/png', function () {
const contentType = this.ContentTypeMapper.map('example.png')
return contentType.should.equal('image/png')
})
it('should map .jpeg to image/jpeg', function () {
const contentType = this.ContentTypeMapper.map('example.jpeg')
return contentType.should.equal('image/jpeg')
})
return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function () {
const contentType = this.ContentTypeMapper.map('example.svg')
return contentType.should.equal('text/plain')
})
})
})
})

View File

@@ -0,0 +1,246 @@
/* eslint-disable
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/DockerLockManager'
)
describe('DockerLockManager', function () {
beforeEach(function () {
return (this.LockManager = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': (this.Settings = { clsi: { docker: {} } }),
},
}))
})
return describe('runWithLock', function () {
describe('with a single lock', function () {
beforeEach(function (done) {
this.callback = sinon.stub()
return this.LockManager.runWithLock(
'lock-one',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world'), 100),
(err, ...args) => {
this.callback(err, ...Array.from(args))
return done()
}
)
})
return it('should call the callback', function () {
return this.callback
.calledWith(null, 'hello', 'world')
.should.equal(true)
})
})
describe('with two locks', function () {
beforeEach(function (done) {
this.callback1 = sinon.stub()
this.callback2 = sinon.stub()
this.LockManager.runWithLock(
'lock-one',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100),
(err, ...args) => {
return this.callback1(err, ...Array.from(args))
}
)
return this.LockManager.runWithLock(
'lock-two',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200),
(err, ...args) => {
this.callback2(err, ...Array.from(args))
return done()
}
)
})
it('should call the first callback', function () {
return this.callback1
.calledWith(null, 'hello', 'world', 'one')
.should.equal(true)
})
return it('should call the second callback', function () {
return this.callback2
.calledWith(null, 'hello', 'world', 'two')
.should.equal(true)
})
})
return describe('with lock contention', function () {
describe('where the first lock is released quickly', function () {
beforeEach(function (done) {
this.LockManager.MAX_LOCK_WAIT_TIME = 1000
this.LockManager.LOCK_TEST_INTERVAL = 100
this.callback1 = sinon.stub()
this.callback2 = sinon.stub()
this.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100),
(err, ...args) => {
return this.callback1(err, ...Array.from(args))
}
)
return this.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200),
(err, ...args) => {
this.callback2(err, ...Array.from(args))
return done()
}
)
})
it('should call the first callback', function () {
return this.callback1
.calledWith(null, 'hello', 'world', 'one')
.should.equal(true)
})
return it('should call the second callback', function () {
return this.callback2
.calledWith(null, 'hello', 'world', 'two')
.should.equal(true)
})
})
describe('where the first lock is held longer than the waiting time', function () {
beforeEach(function (done) {
let doneTwo
this.LockManager.MAX_LOCK_HOLD_TIME = 10000
this.LockManager.MAX_LOCK_WAIT_TIME = 1000
this.LockManager.LOCK_TEST_INTERVAL = 100
this.callback1 = sinon.stub()
this.callback2 = sinon.stub()
let doneOne = (doneTwo = false)
const finish = function (key) {
if (key === 1) {
doneOne = true
}
if (key === 2) {
doneTwo = true
}
if (doneOne && doneTwo) {
return done()
}
}
this.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(
() => releaseLock(null, 'hello', 'world', 'one'),
1100
),
(err, ...args) => {
this.callback1(err, ...Array.from(args))
return finish(1)
}
)
return this.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100),
(err, ...args) => {
this.callback2(err, ...Array.from(args))
return finish(2)
}
)
})
it('should call the first callback', function () {
return this.callback1
.calledWith(null, 'hello', 'world', 'one')
.should.equal(true)
})
return it('should call the second callback with an error', function () {
const error = sinon.match.instanceOf(Error)
return this.callback2.calledWith(error).should.equal(true)
})
})
return describe('where the first lock is held longer than the max holding time', function () {
beforeEach(function (done) {
let doneTwo
this.LockManager.MAX_LOCK_HOLD_TIME = 1000
this.LockManager.MAX_LOCK_WAIT_TIME = 2000
this.LockManager.LOCK_TEST_INTERVAL = 100
this.callback1 = sinon.stub()
this.callback2 = sinon.stub()
let doneOne = (doneTwo = false)
const finish = function (key) {
if (key === 1) {
doneOne = true
}
if (key === 2) {
doneTwo = true
}
if (doneOne && doneTwo) {
return done()
}
}
this.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(
() => releaseLock(null, 'hello', 'world', 'one'),
1500
),
(err, ...args) => {
this.callback1(err, ...Array.from(args))
return finish(1)
}
)
return this.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100),
(err, ...args) => {
this.callback2(err, ...Array.from(args))
return finish(2)
}
)
})
it('should call the first callback', function () {
return this.callback1
.calledWith(null, 'hello', 'world', 'one')
.should.equal(true)
})
return it('should call the second callback', function () {
return this.callback2
.calledWith(null, 'hello', 'world', 'two')
.should.equal(true)
})
})
})
})
})

View File

@@ -0,0 +1,981 @@
/* eslint-disable
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/DockerRunner'
)
const Path = require('node:path')
describe('DockerRunner', function () {
beforeEach(function () {
let container, Docker, Timer
this.container = container = {}
this.DockerRunner = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': (this.Settings = {
clsi: { docker: {} },
path: {},
}),
dockerode: (Docker = (function () {
Docker = class Docker {
static initClass() {
this.prototype.getContainer = sinon.stub().returns(container)
this.prototype.createContainer = sinon
.stub()
.yields(null, container)
this.prototype.listContainers = sinon.stub()
}
}
Docker.initClass()
return Docker
})()),
fs: (this.fs = {
stat: sinon.stub().yields(null, {
isDirectory() {
return true
},
}),
}),
'./Metrics': {
Timer: (Timer = class Timer {
done() {}
}),
},
'./LockManager': {
runWithLock(key, runner, callback) {
return runner(callback)
},
},
},
globals: { Math }, // used by lodash
})
this.Docker = Docker
this.getContainer = Docker.prototype.getContainer
this.createContainer = Docker.prototype.createContainer
this.listContainers = Docker.prototype.listContainers
this.directory = '/local/compile/directory'
this.mainFile = 'main-file.tex'
this.compiler = 'pdflatex'
this.image = 'example.com/overleaf/image:2016.2'
this.env = {}
this.callback = sinon.stub()
this.project_id = 'project-id-123'
this.volumes = { '/some/host/dir/compiles/directory': '/compile' }
this.Settings.clsi.docker.image = this.defaultImage = 'default-image'
this.Settings.path.sandboxedCompilesHostDirCompiles =
'/some/host/dir/compiles'
this.Settings.path.sandboxedCompilesHostDirOutput = '/some/host/dir/output'
this.compileGroup = 'compile-group'
return (this.Settings.clsi.docker.env = { PATH: 'mock-path' })
})
afterEach(function () {
this.DockerRunner.stopContainerMonitor()
})
describe('run', function () {
beforeEach(function (done) {
this.DockerRunner._getContainerOptions = sinon
.stub()
.returns((this.options = { mockoptions: 'foo' }))
this.DockerRunner._fingerprintContainer = sinon
.stub()
.returns((this.fingerprint = 'fingerprint'))
this.name = `project-${this.project_id}-${this.fingerprint}`
this.command = ['mock', 'command', '--outdir=$COMPILE_DIR']
this.command_with_dir = ['mock', 'command', '--outdir=/compile']
this.timeout = 42000
return done()
})
describe('successfully', function () {
beforeEach(function (done) {
this.DockerRunner._runAndWaitForContainer = sinon
.stub()
.callsArgWith(3, null, (this.output = 'mock-output'))
return this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
this.image,
this.timeout,
this.env,
this.compileGroup,
(err, output) => {
this.callback(err, output)
return done()
}
)
})
it('should generate the options for the container', function () {
return this.DockerRunner._getContainerOptions
.calledWith(
this.command_with_dir,
this.image,
this.volumes,
this.timeout
)
.should.equal(true)
})
it('should generate the fingerprint from the returned options', function () {
return this.DockerRunner._fingerprintContainer
.calledWith(this.options)
.should.equal(true)
})
it('should do the run', function () {
return this.DockerRunner._runAndWaitForContainer
.calledWith(this.options, this.volumes, this.timeout)
.should.equal(true)
})
return it('should call the callback', function () {
return this.callback.calledWith(null, this.output).should.equal(true)
})
})
describe('standard compile', function () {
beforeEach(function () {
this.directory = '/var/lib/overleaf/data/compiles/xyz'
this.DockerRunner._runAndWaitForContainer = sinon
.stub()
.callsArgWith(3, null, (this.output = 'mock-output'))
return this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
this.image,
this.timeout,
this.env,
this.compileGroup,
this.callback
)
})
it('should re-write the bind directory', function () {
const volumes =
this.DockerRunner._runAndWaitForContainer.lastCall.args[1]
return expect(volumes).to.deep.equal({
'/some/host/dir/compiles/xyz': '/compile',
})
})
return it('should call the callback', function () {
return this.callback.calledWith(null, this.output).should.equal(true)
})
})
describe('synctex-output', function () {
beforeEach(function () {
this.directory = '/var/lib/overleaf/data/output/xyz/generated-files/id'
this.DockerRunner._runAndWaitForContainer = sinon
.stub()
.callsArgWith(3, null, (this.output = 'mock-output'))
this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
this.image,
this.timeout,
this.env,
'synctex-output',
this.callback
)
})
it('should re-write the bind directory and set ro flag', function () {
const volumes =
this.DockerRunner._runAndWaitForContainer.lastCall.args[1]
expect(volumes).to.deep.equal({
'/some/host/dir/output/xyz/generated-files/id': '/compile:ro',
})
})
it('should call the callback', function () {
this.callback.calledWith(null, this.output).should.equal(true)
})
})
describe('synctex', function () {
beforeEach(function () {
this.directory = '/var/lib/overleaf/data/compile/xyz'
this.DockerRunner._runAndWaitForContainer = sinon
.stub()
.callsArgWith(3, null, (this.output = 'mock-output'))
this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
this.image,
this.timeout,
this.env,
'synctex',
this.callback
)
})
it('should re-write the bind directory', function () {
const volumes =
this.DockerRunner._runAndWaitForContainer.lastCall.args[1]
expect(volumes).to.deep.equal({
'/some/host/dir/compiles/xyz': '/compile:ro',
})
})
it('should call the callback', function () {
this.callback.calledWith(null, this.output).should.equal(true)
})
})
describe('wordcount', function () {
beforeEach(function () {
this.directory = '/var/lib/overleaf/data/compile/xyz'
this.DockerRunner._runAndWaitForContainer = sinon
.stub()
.callsArgWith(3, null, (this.output = 'mock-output'))
this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
this.image,
this.timeout,
this.env,
'wordcount',
this.callback
)
})
it('should re-write the bind directory', function () {
const volumes =
this.DockerRunner._runAndWaitForContainer.lastCall.args[1]
expect(volumes).to.deep.equal({
'/some/host/dir/compiles/xyz': '/compile:ro',
})
})
it('should call the callback', function () {
this.callback.calledWith(null, this.output).should.equal(true)
})
})
describe('when the run throws an error', function () {
beforeEach(function () {
let firstTime = true
this.output = 'mock-output'
this.DockerRunner._runAndWaitForContainer = (
options,
volumes,
timeout,
callback
) => {
if (callback == null) {
callback = function () {}
}
if (firstTime) {
firstTime = false
const error = new Error('(HTTP code 500) server error - ...')
error.statusCode = 500
return callback(error)
} else {
return callback(null, this.output)
}
}
sinon.spy(this.DockerRunner, '_runAndWaitForContainer')
this.DockerRunner.destroyContainer = sinon.stub().callsArg(3)
return this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
this.image,
this.timeout,
this.env,
this.compileGroup,
this.callback
)
})
it('should do the run twice', function () {
return this.DockerRunner._runAndWaitForContainer.calledTwice.should.equal(
true
)
})
it('should destroy the container in between', function () {
return this.DockerRunner.destroyContainer
.calledWith(this.name, null)
.should.equal(true)
})
return it('should call the callback', function () {
return this.callback.calledWith(null, this.output).should.equal(true)
})
})
describe('with no image', function () {
beforeEach(function () {
this.DockerRunner._runAndWaitForContainer = sinon
.stub()
.callsArgWith(3, null, (this.output = 'mock-output'))
return this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
null,
this.timeout,
this.env,
this.compileGroup,
this.callback
)
})
return it('should use the default image', function () {
return this.DockerRunner._getContainerOptions
.calledWith(
this.command_with_dir,
this.defaultImage,
this.volumes,
this.timeout
)
.should.equal(true)
})
})
describe('with image override', function () {
beforeEach(function () {
this.Settings.texliveImageNameOveride = 'overrideimage.com/something'
this.DockerRunner._runAndWaitForContainer = sinon
.stub()
.callsArgWith(3, null, (this.output = 'mock-output'))
return this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
this.image,
this.timeout,
this.env,
this.compileGroup,
this.callback
)
})
return it('should use the override and keep the tag', function () {
const image = this.DockerRunner._getContainerOptions.args[0][1]
return image.should.equal('overrideimage.com/something/image:2016.2')
})
})
describe('with image restriction', function () {
beforeEach(function () {
this.Settings.clsi.docker.allowedImages = [
'repo/image:tag1',
'repo/image:tag2',
]
this.DockerRunner._runAndWaitForContainer = sinon
.stub()
.callsArgWith(3, null, (this.output = 'mock-output'))
})
describe('with a valid image', function () {
beforeEach(function () {
this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
'repo/image:tag1',
this.timeout,
this.env,
this.compileGroup,
this.callback
)
})
it('should setup the container', function () {
this.DockerRunner._getContainerOptions.called.should.equal(true)
})
})
describe('with a invalid image', function () {
beforeEach(function () {
this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
'something/different:evil',
this.timeout,
this.env,
this.compileGroup,
this.callback
)
})
it('should call the callback with an error', function () {
const err = new Error('image not allowed')
this.callback.called.should.equal(true)
this.callback.args[0][0].message.should.equal(err.message)
})
it('should not setup the container', function () {
this.DockerRunner._getContainerOptions.called.should.equal(false)
})
})
})
})
describe('run with _getOptions', function () {
beforeEach(function (done) {
// this.DockerRunner._getContainerOptions = sinon
// .stub()
// .returns((this.options = { mockoptions: 'foo' }))
this.DockerRunner._fingerprintContainer = sinon
.stub()
.returns((this.fingerprint = 'fingerprint'))
this.name = `project-${this.project_id}-${this.fingerprint}`
this.command = ['mock', 'command', '--outdir=$COMPILE_DIR']
this.command_with_dir = ['mock', 'command', '--outdir=/compile']
this.timeout = 42000
return done()
})
describe('when a compile group config is set', function () {
beforeEach(function () {
this.Settings.clsi.docker.compileGroupConfig = {
'compile-group': {
'HostConfig.newProperty': 'new-property',
},
'other-group': { otherProperty: 'other-property' },
}
this.DockerRunner._runAndWaitForContainer = sinon
.stub()
.callsArgWith(3, null, (this.output = 'mock-output'))
return this.DockerRunner.run(
this.project_id,
this.command,
this.directory,
this.image,
this.timeout,
this.env,
this.compileGroup,
this.callback
)
})
it('should set the docker options for the compile group', function () {
const options =
this.DockerRunner._runAndWaitForContainer.lastCall.args[0]
return expect(options.HostConfig).to.deep.include({
Binds: ['/some/host/dir/compiles/directory:/compile:rw'],
LogConfig: { Type: 'none', Config: {} },
CapDrop: 'ALL',
SecurityOpt: ['no-new-privileges'],
newProperty: 'new-property',
})
})
return it('should call the callback', function () {
return this.callback.calledWith(null, this.output).should.equal(true)
})
})
})
describe('_runAndWaitForContainer', function () {
beforeEach(function () {
this.options = { mockoptions: 'foo', name: (this.name = 'mock-name') }
this.DockerRunner.startContainer = (
options,
volumes,
attachStreamHandler,
callback
) => {
attachStreamHandler(null, (this.output = 'mock-output'))
return callback(null, (this.containerId = 'container-id'))
}
sinon.spy(this.DockerRunner, 'startContainer')
this.DockerRunner.waitForContainer = sinon
.stub()
.callsArgWith(2, null, (this.exitCode = 42))
return this.DockerRunner._runAndWaitForContainer(
this.options,
this.volumes,
this.timeout,
this.callback
)
})
it('should create/start the container', function () {
return this.DockerRunner.startContainer
.calledWith(this.options, this.volumes)
.should.equal(true)
})
it('should wait for the container to finish', function () {
return this.DockerRunner.waitForContainer
.calledWith(this.name, this.timeout)
.should.equal(true)
})
return it('should call the callback with the output', function () {
return this.callback.calledWith(null, this.output).should.equal(true)
})
})
describe('startContainer', function () {
beforeEach(function () {
this.attachStreamHandler = sinon.stub()
this.attachStreamHandler.cock = true
this.options = { mockoptions: 'foo', name: 'mock-name' }
this.container.inspect = sinon.stub().callsArgWith(0)
this.DockerRunner.attachToContainer = (
containerId,
attachStreamHandler,
cb
) => {
attachStreamHandler()
return cb()
}
return sinon.spy(this.DockerRunner, 'attachToContainer')
})
describe('when the container exists', function () {
beforeEach(function () {
this.container.inspect = sinon.stub().callsArgWith(0)
this.container.start = sinon.stub().yields()
return this.DockerRunner.startContainer(
this.options,
this.volumes,
() => {},
this.callback
)
})
it('should start the container with the given name', function () {
this.getContainer.calledWith(this.options.name).should.equal(true)
return this.container.start.called.should.equal(true)
})
it('should not try to create the container', function () {
return this.createContainer.called.should.equal(false)
})
it('should attach to the container', function () {
return this.DockerRunner.attachToContainer.called.should.equal(true)
})
it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
return it('should attach before the container starts', function () {
return sinon.assert.callOrder(
this.DockerRunner.attachToContainer,
this.container.start
)
})
})
describe('when the container does not exist', function () {
beforeEach(function () {
const exists = false
this.container.start = sinon.stub().yields()
this.container.inspect = sinon
.stub()
.callsArgWith(0, { statusCode: 404 })
return this.DockerRunner.startContainer(
this.options,
this.volumes,
this.attachStreamHandler,
this.callback
)
})
it('should create the container', function () {
return this.createContainer.calledWith(this.options).should.equal(true)
})
it('should call the callback and stream handler', function () {
this.attachStreamHandler.called.should.equal(true)
return this.callback.called.should.equal(true)
})
it('should attach to the container', function () {
return this.DockerRunner.attachToContainer.called.should.equal(true)
})
return it('should attach before the container starts', function () {
return sinon.assert.callOrder(
this.DockerRunner.attachToContainer,
this.container.start
)
})
})
describe('when the container is already running', function () {
beforeEach(function () {
const error = new Error(
`HTTP code is 304 which indicates error: server error - start: Cannot start container ${this.name}: The container MOCKID is already running.`
)
error.statusCode = 304
this.container.start = sinon.stub().yields(error)
this.container.inspect = sinon.stub().callsArgWith(0)
return this.DockerRunner.startContainer(
this.options,
this.volumes,
this.attachStreamHandler,
this.callback
)
})
it('should not try to create the container', function () {
return this.createContainer.called.should.equal(false)
})
return it('should call the callback and stream handler without an error', function () {
this.attachStreamHandler.called.should.equal(true)
return this.callback.called.should.equal(true)
})
})
return describe('when the container tries to be created, but already has been (race condition)', function () {})
})
describe('waitForContainer', function () {
beforeEach(function () {
this.containerId = 'container-id'
this.timeout = 5000
this.container.wait = sinon
.stub()
.yields(null, { StatusCode: (this.statusCode = 42) })
return (this.container.kill = sinon.stub().yields())
})
describe('when the container returns in time', function () {
beforeEach(function () {
return this.DockerRunner.waitForContainer(
this.containerId,
this.timeout,
this.callback
)
})
it('should wait for the container', function () {
this.getContainer.calledWith(this.containerId).should.equal(true)
return this.container.wait.called.should.equal(true)
})
return it('should call the callback with the exit', function () {
return this.callback
.calledWith(null, this.statusCode)
.should.equal(true)
})
})
return describe('when the container does not return before the timeout', function () {
beforeEach(function (done) {
this.container.wait = function (callback) {
if (callback == null) {
callback = function () {}
}
return setTimeout(() => callback(null, { StatusCode: 42 }), 100)
}
this.timeout = 5
return this.DockerRunner.waitForContainer(
this.containerId,
this.timeout,
(...args) => {
this.callback(...Array.from(args || []))
return done()
}
)
})
it('should call kill on the container', function () {
this.getContainer.calledWith(this.containerId).should.equal(true)
return this.container.kill.called.should.equal(true)
})
it('should call the callback with an error', function () {
this.callback.calledWith(sinon.match(Error)).should.equal(true)
const errorObj = this.callback.args[0][0]
expect(errorObj.message).to.include('container timed out')
expect(errorObj.timedout).equal(true)
})
})
})
describe('destroyOldContainers', function () {
beforeEach(function (done) {
const oneHourInSeconds = 60 * 60
const oneHourInMilliseconds = oneHourInSeconds * 1000
const nowInSeconds = Date.now() / 1000
this.containers = [
{
Name: '/project-old-container-name',
Id: 'old-container-id',
Created: nowInSeconds - oneHourInSeconds - 100,
},
{
Name: '/project-new-container-name',
Id: 'new-container-id',
Created: nowInSeconds - oneHourInSeconds + 100,
},
{
Name: '/totally-not-a-project-container',
Id: 'some-random-id',
Created: nowInSeconds - 2 * oneHourInSeconds,
},
]
this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds
this.listContainers.callsArgWith(1, null, this.containers)
this.DockerRunner.destroyContainer = sinon.stub().callsArg(3)
return this.DockerRunner.destroyOldContainers(error => {
this.callback(error)
return done()
})
})
it('should list all containers', function () {
return this.listContainers.calledWith({ all: true }).should.equal(true)
})
it('should destroy old containers', function () {
this.DockerRunner.destroyContainer.callCount.should.equal(1)
return this.DockerRunner.destroyContainer
.calledWith('project-old-container-name', 'old-container-id')
.should.equal(true)
})
it('should not destroy new containers', function () {
return this.DockerRunner.destroyContainer
.calledWith('project-new-container-name', 'new-container-id')
.should.equal(false)
})
it('should not destroy non-project containers', function () {
return this.DockerRunner.destroyContainer
.calledWith('totally-not-a-project-container', 'some-random-id')
.should.equal(false)
})
return it('should callback the callback', function () {
return this.callback.called.should.equal(true)
})
})
describe('_destroyContainer', function () {
beforeEach(function () {
this.containerId = 'some_id'
this.fakeContainer = { remove: sinon.stub().callsArgWith(1, null) }
return (this.Docker.prototype.getContainer = sinon
.stub()
.returns(this.fakeContainer))
})
it('should get the container', function (done) {
return this.DockerRunner._destroyContainer(
this.containerId,
false,
err => {
if (err) return done(err)
this.Docker.prototype.getContainer.callCount.should.equal(1)
this.Docker.prototype.getContainer
.calledWith(this.containerId)
.should.equal(true)
return done()
}
)
})
it('should try to force-destroy the container when shouldForce=true', function (done) {
return this.DockerRunner._destroyContainer(
this.containerId,
true,
err => {
if (err) return done(err)
this.fakeContainer.remove.callCount.should.equal(1)
this.fakeContainer.remove
.calledWith({ force: true, v: true })
.should.equal(true)
return done()
}
)
})
it('should not try to force-destroy the container when shouldForce=false', function (done) {
return this.DockerRunner._destroyContainer(
this.containerId,
false,
err => {
if (err) return done(err)
this.fakeContainer.remove.callCount.should.equal(1)
this.fakeContainer.remove
.calledWith({ force: false, v: true })
.should.equal(true)
return done()
}
)
})
it('should not produce an error', function (done) {
return this.DockerRunner._destroyContainer(
this.containerId,
false,
err => {
expect(err).to.equal(null)
return done()
}
)
})
describe('when the container is already gone', function () {
beforeEach(function () {
this.fakeError = new Error('woops')
this.fakeError.statusCode = 404
this.fakeContainer = {
remove: sinon.stub().callsArgWith(1, this.fakeError),
}
return (this.Docker.prototype.getContainer = sinon
.stub()
.returns(this.fakeContainer))
})
return it('should not produce an error', function (done) {
return this.DockerRunner._destroyContainer(
this.containerId,
false,
err => {
expect(err).to.equal(null)
return done()
}
)
})
})
return describe('when container.destroy produces an error', function (done) {
beforeEach(function () {
this.fakeError = new Error('woops')
this.fakeError.statusCode = 500
this.fakeContainer = {
remove: sinon.stub().callsArgWith(1, this.fakeError),
}
return (this.Docker.prototype.getContainer = sinon
.stub()
.returns(this.fakeContainer))
})
return it('should produce an error', function (done) {
return this.DockerRunner._destroyContainer(
this.containerId,
false,
err => {
expect(err).to.not.equal(null)
expect(err).to.equal(this.fakeError)
return done()
}
)
})
})
})
return describe('kill', function () {
beforeEach(function () {
this.containerId = 'some_id'
this.fakeContainer = { kill: sinon.stub().callsArgWith(0, null) }
return (this.Docker.prototype.getContainer = sinon
.stub()
.returns(this.fakeContainer))
})
it('should get the container', function (done) {
return this.DockerRunner.kill(this.containerId, err => {
if (err) return done(err)
this.Docker.prototype.getContainer.callCount.should.equal(1)
this.Docker.prototype.getContainer
.calledWith(this.containerId)
.should.equal(true)
return done()
})
})
it('should try to force-destroy the container', function (done) {
return this.DockerRunner.kill(this.containerId, err => {
if (err) return done(err)
this.fakeContainer.kill.callCount.should.equal(1)
return done()
})
})
it('should not produce an error', function (done) {
return this.DockerRunner.kill(this.containerId, err => {
expect(err).to.equal(undefined)
return done()
})
})
describe('when the container is not actually running', function () {
beforeEach(function () {
this.fakeError = new Error('woops')
this.fakeError.statusCode = 500
this.fakeError.message =
'Cannot kill container <whatever> is not running'
this.fakeContainer = {
kill: sinon.stub().callsArgWith(0, this.fakeError),
}
return (this.Docker.prototype.getContainer = sinon
.stub()
.returns(this.fakeContainer))
})
return it('should not produce an error', function (done) {
return this.DockerRunner.kill(this.containerId, err => {
expect(err).to.equal(undefined)
return done()
})
})
})
return describe('when container.kill produces a legitimate error', function (done) {
beforeEach(function () {
this.fakeError = new Error('woops')
this.fakeError.statusCode = 500
this.fakeError.message = 'Totally legitimate reason to throw an error'
this.fakeContainer = {
kill: sinon.stub().callsArgWith(0, this.fakeError),
}
return (this.Docker.prototype.getContainer = sinon
.stub()
.returns(this.fakeContainer))
})
return it('should produce an error', function (done) {
return this.DockerRunner.kill(this.containerId, err => {
expect(err).to.not.equal(undefined)
expect(err).to.equal(this.fakeError)
return done()
})
})
})
})
})

View File

@@ -0,0 +1,44 @@
const Path = require('node:path')
const fsPromises = require('node:fs/promises')
const { expect } = require('chai')
const mockFs = require('mock-fs')
const SandboxedModule = require('sandboxed-module')
const MODULE_PATH = Path.join(__dirname, '../../../app/js/DraftModeManager')
describe('DraftModeManager', function () {
beforeEach(function () {
this.DraftModeManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'fs/promises': fsPromises,
},
})
this.filename = '/mock/filename.tex'
this.contents = `\
\\documentclass{article}
\\begin{document}
Hello world
\\end{document}\
`
mockFs({
[this.filename]: this.contents,
})
})
afterEach(function () {
mockFs.restore()
})
describe('injectDraftMode', function () {
it('prepends a special command to the beginning of the file', async function () {
await this.DraftModeManager.promises.injectDraftMode(this.filename)
const contents = await fsPromises.readFile(this.filename, {
encoding: 'utf8',
})
expect(contents).to.equal(
'\\PassOptionsToPackage{draft}{graphicx}\\PassOptionsToPackage{draft}{graphics}' +
this.contents
)
})
})
})

View File

@@ -0,0 +1,219 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const MODULE_PATH = require('node:path').join(
__dirname,
'../../../app/js/LatexRunner'
)
describe('LatexRunner', function () {
beforeEach(function () {
this.Settings = {
docker: {
socketPath: '/var/run/docker.sock',
},
}
this.commandRunnerOutput = {
stdout: 'this is stdout',
stderr: 'this is stderr',
}
this.CommandRunner = {
run: sinon.stub().yields(null, this.commandRunnerOutput),
}
this.fs = {
writeFile: sinon.stub().yields(),
unlink: sinon
.stub()
.yields(new Error('ENOENT: no such file or directory, unlink ...')),
}
this.LatexRunner = SandboxedModule.require(MODULE_PATH, {
requires: {
'@overleaf/settings': this.Settings,
'./CommandRunner': this.CommandRunner,
fs: this.fs,
},
})
this.directory = '/local/compile/directory'
this.mainFile = 'main-file.tex'
this.compiler = 'pdflatex'
this.image = 'example.com/image'
this.compileGroup = 'compile-group'
this.callback = sinon.stub()
this.project_id = 'project-id-123'
this.env = { foo: '123' }
this.timeout = 42000
this.flags = []
this.stopOnFirstError = false
this.stats = {}
this.timings = {}
this.call = function (callback) {
this.LatexRunner.runLatex(
this.project_id,
{
directory: this.directory,
mainFile: this.mainFile,
compiler: this.compiler,
timeout: this.timeout,
image: this.image,
environment: this.env,
compileGroup: this.compileGroup,
flags: this.flags,
stopOnFirstError: this.stopOnFirstError,
timings: this.timings,
stats: this.stats,
},
callback
)
}
})
describe('runLatex', function () {
describe('normally', function () {
beforeEach(function (done) {
this.call(done)
})
it('should run the latex command', function () {
this.CommandRunner.run.should.have.been.calledWith(
this.project_id,
[
'latexmk',
'-cd',
'-jobname=output',
'-auxdir=$COMPILE_DIR',
'-outdir=$COMPILE_DIR',
'-synctex=1',
'-interaction=batchmode',
'-f',
'-pdf',
'$COMPILE_DIR/main-file.tex',
],
this.directory,
this.image,
this.timeout,
this.env,
this.compileGroup
)
})
it('should record the stdout and stderr', function () {
this.fs.writeFile.should.have.been.calledWith(
this.directory + '/' + 'output.stdout',
'this is stdout',
{ flag: 'wx' }
)
this.fs.writeFile.should.have.been.calledWith(
this.directory + '/' + 'output.stderr',
'this is stderr',
{ flag: 'wx' }
)
this.fs.unlink.should.have.been.calledWith(
this.directory + '/' + 'output.stdout'
)
this.fs.unlink.should.have.been.calledWith(
this.directory + '/' + 'output.stderr'
)
})
it('should not record cpu metrics', function () {
expect(this.timings['cpu-percent']).to.not.exist
expect(this.timings['cpu-time']).to.not.exist
expect(this.timings['sys-time']).to.not.exist
})
})
describe('with a different compiler', function () {
beforeEach(function (done) {
this.compiler = 'lualatex'
this.call(done)
})
it('should set the appropriate latexmk flag', function () {
this.CommandRunner.run.should.have.been.calledWith(this.project_id, [
'latexmk',
'-cd',
'-jobname=output',
'-auxdir=$COMPILE_DIR',
'-outdir=$COMPILE_DIR',
'-synctex=1',
'-interaction=batchmode',
'-f',
'-lualatex',
'$COMPILE_DIR/main-file.tex',
])
})
})
describe('with time -v', function () {
beforeEach(function (done) {
this.commandRunnerOutput.stderr =
'\tCommand being timed: "sh -c timeout 1 yes > /dev/null"\n' +
'\tUser time (seconds): 0.28\n' +
'\tSystem time (seconds): 0.70\n' +
'\tPercent of CPU this job got: 98%\n'
this.call(done)
})
it('should record cpu metrics', function () {
expect(this.timings['cpu-percent']).to.equal(98)
expect(this.timings['cpu-time']).to.equal(0.28)
expect(this.timings['sys-time']).to.equal(0.7)
})
})
describe('with an .Rtex main file', function () {
beforeEach(function (done) {
this.mainFile = 'main-file.Rtex'
this.call(done)
})
it('should run the latex command on the equivalent .tex file', function () {
const command = this.CommandRunner.run.args[0][1]
const mainFile = command.slice(-1)[0]
mainFile.should.equal('$COMPILE_DIR/main-file.tex')
})
})
describe('with a flags option', function () {
beforeEach(function (done) {
this.flags = ['-shell-restricted', '-halt-on-error']
this.call(done)
})
it('should include the flags in the command', function () {
const command = this.CommandRunner.run.args[0][1]
const flags = command.filter(
arg => arg === '-shell-restricted' || arg === '-halt-on-error'
)
flags.length.should.equal(2)
flags[0].should.equal('-shell-restricted')
flags[1].should.equal('-halt-on-error')
})
})
describe('with the stopOnFirstError option', function () {
beforeEach(function (done) {
this.stopOnFirstError = true
this.call(done)
})
it('should set the appropriate flags', function () {
this.CommandRunner.run.should.have.been.calledWith(this.project_id, [
'latexmk',
'-cd',
'-jobname=output',
'-auxdir=$COMPILE_DIR',
'-outdir=$COMPILE_DIR',
'-synctex=1',
'-interaction=batchmode',
'-halt-on-error',
'-pdf',
'$COMPILE_DIR/main-file.tex',
])
})
})
})
})

View File

@@ -0,0 +1,116 @@
const { expect } = require('chai')
const sinon = require('sinon')
const SandboxedModule = require('sandboxed-module')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/LockManager'
)
const Errors = require('../../../app/js/Errors')
describe('LockManager', function () {
beforeEach(function () {
this.key = '/local/compile/directory'
this.clock = sinon.useFakeTimers()
this.LockManager = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/metrics': (this.Metrics = {
inc: sinon.stub(),
gauge: sinon.stub(),
}),
'@overleaf/settings': (this.Settings = {
compileConcurrencyLimit: 5,
}),
'./Errors': (this.Erros = Errors),
'./RequestParser': { MAX_TIMEOUT: 600 },
},
})
})
afterEach(function () {
this.clock.restore()
})
describe('when the lock is available', function () {
it('the lock can be acquired', function () {
const lock = this.LockManager.acquire(this.key)
expect(lock).to.exist
lock.release()
})
})
describe('after the lock is acquired', function () {
beforeEach(function () {
this.lock = this.LockManager.acquire(this.key)
})
afterEach(function () {
if (this.lock != null) {
this.lock.release()
}
})
it("the lock can't be acquired again", function () {
expect(() => this.LockManager.acquire(this.key)).to.throw(
Errors.AlreadyCompilingError
)
})
it('another lock can be acquired', function () {
const lock = this.LockManager.acquire('another key')
expect(lock).to.exist
lock.release()
})
it('the lock can be acquired again after an expiry period', function () {
// The expiry time is a little bit over 10 minutes. Let's wait 15 minutes.
this.clock.tick(15 * 60 * 1000)
this.lock = this.LockManager.acquire(this.key)
expect(this.lock).to.exist
})
it('the lock can be acquired again after it was released', function () {
this.lock.release()
this.lock = this.LockManager.acquire(this.key)
expect(this.lock).to.exist
})
})
describe('concurrency limit', function () {
it('exceeding the limit', function () {
for (let i = 0; i <= this.Settings.compileConcurrencyLimit; i++) {
this.LockManager.acquire('test_key' + i)
}
this.Metrics.inc
.calledWith('exceeded-compilier-concurrency-limit')
.should.equal(false)
expect(() =>
this.LockManager.acquire(
'test_key_' + (this.Settings.compileConcurrencyLimit + 1),
false
)
).to.throw(Errors.TooManyCompileRequestsError)
this.Metrics.inc
.calledWith('exceeded-compilier-concurrency-limit')
.should.equal(true)
})
it('within the limit', function () {
for (let i = 0; i <= this.Settings.compileConcurrencyLimit - 1; i++) {
this.LockManager.acquire('test_key' + i)
}
this.Metrics.inc
.calledWith('exceeded-compilier-concurrency-limit')
.should.equal(false)
const lock = this.LockManager.acquire(
'test_key_' + this.Settings.compileConcurrencyLimit,
false
)
expect(lock.key).to.equal(
'test_key_' + this.Settings.compileConcurrencyLimit
)
})
})
})

View File

@@ -0,0 +1,105 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const MODULE_PATH = require('node:path').join(
__dirname,
'../../../app/js/OutputController'
)
describe('OutputController', function () {
describe('createOutputZip', function () {
beforeEach(function () {
this.archive = {}
this.pipeline = sinon.stub().resolves()
this.archiveFilesForBuild = sinon.stub().resolves(this.archive)
this.OutputController = SandboxedModule.require(MODULE_PATH, {
requires: {
'./OutputFileArchiveManager': {
archiveFilesForBuild: this.archiveFilesForBuild,
},
'stream/promises': {
pipeline: this.pipeline,
},
},
})
})
describe('when OutputFileArchiveManager creates an archive', function () {
beforeEach(function (done) {
this.res = {
attachment: sinon.stub(),
setHeader: sinon.stub(),
}
this.req = {
params: {
project_id: 'project-id-123',
user_id: 'user-id-123',
build_id: 'build-id-123',
},
query: {
files: ['output.tex'],
},
}
this.pipeline.callsFake(() => {
done()
return Promise.resolve()
})
this.OutputController.createOutputZip(this.req, this.res)
})
it('creates a pipeline from the archive to the response', function () {
sinon.assert.calledWith(this.pipeline, this.archive, this.res)
})
it('calls the express convenience method to set attachment headers', function () {
sinon.assert.calledWith(this.res.attachment, 'output.zip')
})
it('sets the X-Content-Type-Options header to nosniff', function () {
sinon.assert.calledWith(
this.res.setHeader,
'X-Content-Type-Options',
'nosniff'
)
})
})
describe('when OutputFileArchiveManager throws an error', function () {
let error
beforeEach(function (done) {
error = new Error('error message')
this.archiveFilesForBuild.rejects(error)
this.res = {
status: sinon.stub().returnsThis(),
send: sinon.stub(),
}
this.req = {
params: {
project_id: 'project-id-123',
user_id: 'user-id-123',
build_id: 'build-id-123',
},
query: {
files: ['output.tex'],
},
}
this.OutputController.createOutputZip(
this.req,
this.res,
(this.next = sinon.stub().callsFake(() => {
done()
}))
)
})
it('calls next with the error', function () {
sinon.assert.calledWith(this.next, error)
})
})
})
})

View File

@@ -0,0 +1,229 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { assert, expect } = require('chai')
const MODULE_PATH = require('node:path').join(
__dirname,
'../../../app/js/OutputFileArchiveManager'
)
describe('OutputFileArchiveManager', function () {
const userId = 'user-id-123'
const projectId = 'project-id-123'
const buildId = 'build-id-123'
afterEach(function () {
sinon.restore()
})
beforeEach(function () {
this.OutputFileFinder = {
promises: {
findOutputFiles: sinon.stub().resolves({ outputFiles: [] }),
},
}
this.OutputCacheManger = {
path: sinon.stub().callsFake((build, path) => {
return `${build}/${path}`
}),
}
this.archive = {
append: sinon.stub(),
finalize: sinon.stub().resolves(),
on: sinon.stub(),
}
this.archiver = sinon.stub().returns(this.archive)
this.outputDir = '/output/dir'
this.fs = {
open: sinon.stub().callsFake(file => ({
createReadStream: sinon.stub().returns(`handle: ${file}`),
})),
}
this.OutputFileArchiveManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'./OutputFileFinder': this.OutputFileFinder,
'./OutputCacheManager': this.OutputCacheManger,
archiver: this.archiver,
'fs/promises': this.fs,
'@overleaf/settings': {
path: {
outputDir: this.outputDir,
},
},
},
})
})
describe('when the output cache directory contains only exportable files', function () {
beforeEach(async function () {
this.OutputFileFinder.promises.findOutputFiles.resolves({
outputFiles: [
{ path: 'file_1' },
{ path: 'file_2' },
{ path: 'file_3' },
{ path: 'file_4' },
],
})
await this.OutputFileArchiveManager.archiveFilesForBuild(
projectId,
userId,
buildId
)
})
it('creates a zip archive', function () {
sinon.assert.calledWith(this.archiver, 'zip')
})
it('listens to errors from the archive', function () {
sinon.assert.calledWith(this.archive.on, 'error', sinon.match.func)
})
it('adds all the output files to the archive', function () {
expect(this.archive.append.callCount).to.equal(4)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
sinon.match({ name: 'file_1' })
)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
sinon.match({ name: 'file_2' })
)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
sinon.match({ name: 'file_3' })
)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
sinon.match({ name: 'file_4' })
)
})
it('finalizes the archive after all files are appended', function () {
sinon.assert.called(this.archive.finalize)
expect(this.archive.finalize.calledBefore(this.archive.append)).to.be
.false
})
})
describe('when the directory includes files ignored by web', function () {
beforeEach(async function () {
this.OutputFileFinder.promises.findOutputFiles.resolves({
outputFiles: [
{ path: 'file_1' },
{ path: 'file_2' },
{ path: 'file_3' },
{ path: 'file_4' },
{ path: 'output.pdf' },
],
})
await this.OutputFileArchiveManager.archiveFilesForBuild(
projectId,
userId,
buildId
)
})
it('only includes the non-ignored files in the archive', function () {
expect(this.archive.append.callCount).to.equal(4)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
sinon.match({ name: 'file_1' })
)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
sinon.match({ name: 'file_2' })
)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
sinon.match({ name: 'file_3' })
)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
sinon.match({ name: 'file_4' })
)
})
})
describe('when one of the files is called output.pdf', function () {
beforeEach(async function () {
this.OutputFileFinder.promises.findOutputFiles.resolves({
outputFiles: [
{ path: 'file_1' },
{ path: 'file_2' },
{ path: 'file_3' },
{ path: 'file_4' },
{ path: 'output.pdf' },
],
})
await this.OutputFileArchiveManager.archiveFilesForBuild(
projectId,
userId,
buildId
)
})
it('does not include that file in the archive', function () {
expect(this.archive.append.callCount).to.equal(4)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
sinon.match({ name: 'file_1' })
)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
sinon.match({ name: 'file_2' })
)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
sinon.match({ name: 'file_3' })
)
sinon.assert.calledWith(
this.archive.append,
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
sinon.match({ name: 'file_4' })
)
})
})
describe('when the output directory cannot be accessed', function () {
beforeEach(async function () {
this.OutputFileFinder.promises.findOutputFiles.rejects({
code: 'ENOENT',
})
})
it('rejects with a NotFoundError', async function () {
try {
await this.OutputFileArchiveManager.archiveFilesForBuild(
projectId,
userId,
buildId
)
assert.fail('should have thrown a NotFoundError')
} catch (err) {
expect(err).to.haveOwnProperty('name', 'NotFoundError')
}
})
it('does not create an archive', function () {
expect(this.archiver.called).to.be.false
})
})
})

View File

@@ -0,0 +1,72 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/OutputFileFinder'
)
const { expect } = require('chai')
const mockFs = require('mock-fs')
describe('OutputFileFinder', function () {
beforeEach(function () {
this.OutputFileFinder = SandboxedModule.require(modulePath, {})
this.directory = '/test/dir'
this.callback = sinon.stub()
mockFs({
[this.directory]: {
resource: {
'path.tex': 'a source file',
},
'output.pdf': 'a generated pdf file',
extra: {
'file.tex': 'a generated tex file',
},
'sneaky-file': mockFs.symlink({
path: '../foo',
}),
},
})
})
afterEach(function () {
mockFs.restore()
})
describe('findOutputFiles', function () {
beforeEach(async function () {
this.resource_path = 'resource/path.tex'
this.output_paths = ['output.pdf', 'extra/file.tex']
this.all_paths = this.output_paths.concat([this.resource_path])
this.resources = [{ path: (this.resource_path = 'resource/path.tex') }]
const { outputFiles, allEntries } =
await this.OutputFileFinder.promises.findOutputFiles(
this.resources,
this.directory
)
this.outputFiles = outputFiles
this.allEntries = allEntries
})
it('should only return the output files, not directories or resource paths', function () {
expect(this.outputFiles).to.have.deep.members([
{
path: 'output.pdf',
type: 'pdf',
},
{
path: 'extra/file.tex',
type: 'tex',
},
])
expect(this.allEntries).to.deep.equal([
'extra/file.tex',
'extra/',
'output.pdf',
'resource/path.tex',
'resource/',
'sneaky-file',
])
})
})
})

View File

@@ -0,0 +1,192 @@
/* eslint-disable
no-return-assign,
no-unused-vars,
n/no-deprecated-api,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/OutputFileOptimiser'
)
const path = require('node:path')
const { expect } = require('chai')
const { EventEmitter } = require('node:events')
describe('OutputFileOptimiser', function () {
beforeEach(function () {
this.OutputFileOptimiser = SandboxedModule.require(modulePath, {
requires: {
fs: (this.fs = {}),
path: (this.Path = {}),
child_process: { spawn: (this.spawn = sinon.stub()) },
'./Metrics': {},
},
globals: { Math }, // used by lodash
})
this.directory = '/test/dir'
return (this.callback = sinon.stub())
})
describe('optimiseFile', function () {
beforeEach(function () {
this.src = './output.pdf'
return (this.dst = './output.pdf')
})
describe('when the file is not a pdf file', function () {
beforeEach(function (done) {
this.src = './output.log'
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
.stub()
.callsArgWith(1, null, false)
this.OutputFileOptimiser.optimisePDF = sinon
.stub()
.callsArgWith(2, null)
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done)
})
it('should not check if the file is optimised', function () {
return this.OutputFileOptimiser.checkIfPDFIsOptimised
.calledWith(this.src)
.should.equal(false)
})
return it('should not optimise the file', function () {
return this.OutputFileOptimiser.optimisePDF
.calledWith(this.src, this.dst)
.should.equal(false)
})
})
describe('when the pdf file is not optimised', function () {
beforeEach(function (done) {
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
.stub()
.callsArgWith(1, null, false)
this.OutputFileOptimiser.optimisePDF = sinon
.stub()
.callsArgWith(2, null)
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done)
})
it('should check if the pdf is optimised', function () {
return this.OutputFileOptimiser.checkIfPDFIsOptimised
.calledWith(this.src)
.should.equal(true)
})
return it('should optimise the pdf', function () {
return this.OutputFileOptimiser.optimisePDF
.calledWith(this.src, this.dst)
.should.equal(true)
})
})
return describe('when the pdf file is optimised', function () {
beforeEach(function (done) {
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
.stub()
.callsArgWith(1, null, true)
this.OutputFileOptimiser.optimisePDF = sinon
.stub()
.callsArgWith(2, null)
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done)
})
it('should check if the pdf is optimised', function () {
return this.OutputFileOptimiser.checkIfPDFIsOptimised
.calledWith(this.src)
.should.equal(true)
})
return it('should not optimise the pdf', function () {
return this.OutputFileOptimiser.optimisePDF
.calledWith(this.src, this.dst)
.should.equal(false)
})
})
})
return describe('checkIfPDFISOptimised', function () {
beforeEach(function () {
this.callback = sinon.stub()
this.fd = 1234
this.fs.open = sinon.stub().yields(null, this.fd)
this.fs.read = sinon
.stub()
.withArgs(this.fd)
.yields(null, 100, Buffer.from('hello /Linearized 1'))
this.fs.close = sinon.stub().withArgs(this.fd).yields(null)
return this.OutputFileOptimiser.checkIfPDFIsOptimised(
this.src,
this.callback
)
})
describe('for a linearised file', function () {
beforeEach(function () {
this.fs.read = sinon
.stub()
.withArgs(this.fd)
.yields(null, 100, Buffer.from('hello /Linearized 1'))
return this.OutputFileOptimiser.checkIfPDFIsOptimised(
this.src,
this.callback
)
})
it('should open the file', function () {
return this.fs.open.calledWith(this.src, 'r').should.equal(true)
})
it('should read the header', function () {
return this.fs.read.calledWith(this.fd).should.equal(true)
})
it('should close the file', function () {
return this.fs.close.calledWith(this.fd).should.equal(true)
})
return it('should call the callback with a true result', function () {
return this.callback.calledWith(null, true).should.equal(true)
})
})
return describe('for an unlinearised file', function () {
beforeEach(function () {
this.fs.read = sinon
.stub()
.withArgs(this.fd)
.yields(null, 100, Buffer.from('hello not linearized 1'))
return this.OutputFileOptimiser.checkIfPDFIsOptimised(
this.src,
this.callback
)
})
it('should open the file', function () {
return this.fs.open.calledWith(this.src, 'r').should.equal(true)
})
it('should read the header', function () {
return this.fs.read.calledWith(this.fd).should.equal(true)
})
it('should close the file', function () {
return this.fs.close.calledWith(this.fd).should.equal(true)
})
return it('should call the callback with a false result', function () {
return this.callback.calledWith(null, false).should.equal(true)
})
})
})
})

View File

@@ -0,0 +1,174 @@
/* eslint-disable
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const assert = require('chai').assert
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/ProjectPersistenceManager'
)
const tk = require('timekeeper')
describe('ProjectPersistenceManager', function () {
beforeEach(function () {
this.fsPromises = {
statfs: sinon.stub(),
}
this.ProjectPersistenceManager = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/metrics': (this.Metrics = { gauge: sinon.stub() }),
'./UrlCache': (this.UrlCache = {}),
'./CompileManager': (this.CompileManager = {}),
fs: { promises: this.fsPromises },
'@overleaf/settings': (this.settings = {
project_cache_length_ms: 1000,
path: {
compilesDir: '/compiles',
outputDir: '/output',
clsiCacheDir: '/cache',
},
}),
},
})
this.callback = sinon.stub()
this.project_id = 'project-id-123'
return (this.user_id = '1234')
})
describe('refreshExpiryTimeout', function () {
it('should leave expiry alone if plenty of disk', function (done) {
this.fsPromises.statfs.resolves({
blocks: 100,
bsize: 1,
bavail: 40,
})
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
this.Metrics.gauge.should.have.been.calledWith(
'disk_available_percent',
40
)
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(
this.settings.project_cache_length_ms
)
done()
})
})
it('should drop EXPIRY_TIMEOUT 10% if low disk usage', function (done) {
this.fsPromises.statfs.resolves({
blocks: 100,
bsize: 1,
bavail: 5,
})
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
this.Metrics.gauge.should.have.been.calledWith(
'disk_available_percent',
5
)
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(900)
done()
})
})
it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', function (done) {
this.fsPromises.statfs.resolves({
blocks: 100,
bsize: 1,
bavail: 5,
})
this.ProjectPersistenceManager.EXPIRY_TIMEOUT = 500
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
this.Metrics.gauge.should.have.been.calledWith(
'disk_available_percent',
5
)
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(500)
done()
})
})
it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', function (done) {
this.fsPromises.statfs.rejects(new Error())
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(1000)
done()
})
})
})
describe('clearExpiredProjects', function () {
beforeEach(function () {
this.project_ids = ['project-id-1', 'project-id-2']
this.ProjectPersistenceManager._findExpiredProjectIds = sinon
.stub()
.callsArgWith(0, null, this.project_ids)
this.ProjectPersistenceManager.clearProjectFromCache = sinon
.stub()
.callsArg(2)
this.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1)
return this.ProjectPersistenceManager.clearExpiredProjects(this.callback)
})
it('should clear each expired project', function () {
return Array.from(this.project_ids).map(projectId =>
this.ProjectPersistenceManager.clearProjectFromCache
.calledWith(projectId)
.should.equal(true)
)
})
return it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
})
return describe('clearProject', function () {
beforeEach(function () {
this.ProjectPersistenceManager._clearProjectFromDatabase = sinon
.stub()
.callsArg(1)
this.UrlCache.clearProject = sinon.stub().callsArg(2)
this.CompileManager.clearProject = sinon.stub().callsArg(2)
return this.ProjectPersistenceManager.clearProject(
this.project_id,
this.user_id,
this.callback
)
})
it('should clear the project from the database', function () {
return this.ProjectPersistenceManager._clearProjectFromDatabase
.calledWith(this.project_id)
.should.equal(true)
})
it('should clear all the cached Urls for the project', function () {
return this.UrlCache.clearProject
.calledWith(this.project_id)
.should.equal(true)
})
it('should clear the project compile folder', function () {
return this.CompileManager.clearProject
.calledWith(this.project_id, this.user_id)
.should.equal(true)
})
return it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
})
})

View File

@@ -0,0 +1,480 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/RequestParser'
)
const tk = require('timekeeper')
describe('RequestParser', function () {
beforeEach(function () {
tk.freeze()
this.callback = sinon.stub()
this.validResource = {
path: 'main.tex',
date: '12:00 01/02/03',
content: 'Hello world',
}
this.validRequest = {
compile: {
token: 'token-123',
options: {
imageName: 'basicImageName/here:2017-1',
compiler: 'pdflatex',
timeout: 42,
},
resources: [],
},
}
this.RequestParser = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': (this.settings = {}),
'./OutputCacheManager': { BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/ },
},
})
})
afterEach(function () {
tk.reset()
})
describe('without a top level object', function () {
beforeEach(function () {
this.RequestParser.parse([], this.callback)
})
it('should return an error', function () {
expect(this.callback).to.have.been.called
expect(this.callback.args[0][0].message).to.equal(
'top level object should have a compile attribute'
)
})
})
describe('without a compile attribute', function () {
beforeEach(function () {
this.RequestParser.parse({}, this.callback)
})
it('should return an error', function () {
expect(this.callback).to.have.been.called
expect(this.callback.args[0][0].message).to.equal(
'top level object should have a compile attribute'
)
})
})
describe('without a valid compiler', function () {
beforeEach(function () {
this.validRequest.compile.options.compiler = 'not-a-compiler'
this.RequestParser.parse(this.validRequest, this.callback)
})
it('should return an error', function () {
this.callback
.calledWithMatch({
message:
'compiler attribute should be one of: pdflatex, latex, xelatex, lualatex',
})
.should.equal(true)
})
})
describe('without a compiler specified', function () {
beforeEach(function (done) {
delete this.validRequest.compile.options.compiler
this.RequestParser.parse(this.validRequest, (error, data) => {
if (error) return done(error)
this.data = data
done()
})
})
it('should set the compiler to pdflatex by default', function () {
this.data.compiler.should.equal('pdflatex')
})
})
describe('with imageName set', function () {
beforeEach(function (done) {
this.RequestParser.parse(this.validRequest, (error, data) => {
if (error) return done(error)
this.data = data
done()
})
})
it('should set the imageName', function () {
this.data.imageName.should.equal('basicImageName/here:2017-1')
})
})
describe('when image restrictions are present', function () {
beforeEach(function () {
this.settings.clsi = { docker: {} }
this.settings.clsi.docker.allowedImages = [
'repo/name:tag1',
'repo/name:tag2',
]
})
describe('with imageName set to something invalid', function () {
beforeEach(function () {
const request = this.validRequest
request.compile.options.imageName = 'something/different:latest'
this.RequestParser.parse(request, (error, data) => {
this.error = error
this.data = data
})
})
it('should throw an error for imageName', function () {
expect(String(this.error)).to.include(
'imageName attribute should be one of'
)
})
})
describe('with imageName set to something valid', function () {
beforeEach(function () {
const request = this.validRequest
request.compile.options.imageName = 'repo/name:tag1'
this.RequestParser.parse(request, (error, data) => {
this.error = error
this.data = data
})
})
it('should set the imageName', function () {
this.data.imageName.should.equal('repo/name:tag1')
})
})
})
describe('with flags set', function () {
beforeEach(function (done) {
this.validRequest.compile.options.flags = ['-file-line-error']
this.RequestParser.parse(this.validRequest, (error, data) => {
if (error) return done(error)
this.data = data
done()
})
})
it('should set the flags attribute', function () {
expect(this.data.flags).to.deep.equal(['-file-line-error'])
})
})
describe('with flags not specified', function () {
beforeEach(function (done) {
this.RequestParser.parse(this.validRequest, (error, data) => {
if (error) return done(error)
this.data = data
done()
})
})
it('it should have an empty flags list', function () {
expect(this.data.flags).to.deep.equal([])
})
})
describe('without a timeout specified', function () {
beforeEach(function (done) {
delete this.validRequest.compile.options.timeout
this.RequestParser.parse(this.validRequest, (error, data) => {
if (error) return done(error)
this.data = data
done()
})
})
it('should set the timeout to MAX_TIMEOUT', function () {
this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000)
})
})
describe('with a timeout larger than the maximum', function () {
beforeEach(function (done) {
this.validRequest.compile.options.timeout =
this.RequestParser.MAX_TIMEOUT + 1
this.RequestParser.parse(this.validRequest, (error, data) => {
if (error) return done(error)
this.data = data
done()
})
})
it('should set the timeout to MAX_TIMEOUT', function () {
this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000)
})
})
describe('with a timeout', function () {
beforeEach(function (done) {
this.RequestParser.parse(this.validRequest, (error, data) => {
if (error) return done(error)
this.data = data
done()
})
})
it('should set the timeout (in milliseconds)', function () {
this.data.timeout.should.equal(
this.validRequest.compile.options.timeout * 1000
)
})
})
describe('with a resource without a path', function () {
beforeEach(function () {
delete this.validResource.path
this.validRequest.compile.resources.push(this.validResource)
this.RequestParser.parse(this.validRequest, this.callback)
})
it('should return an error', function () {
this.callback
.calledWithMatch({
message: 'all resources should have a path attribute',
})
.should.equal(true)
})
})
describe('with a resource with a path', function () {
beforeEach(function () {
this.validResource.path = this.path = 'test.tex'
this.validRequest.compile.resources.push(this.validResource)
this.RequestParser.parse(this.validRequest, this.callback)
this.data = this.callback.args[0][1]
})
it('should return the path in the parsed response', function () {
this.data.resources[0].path.should.equal(this.path)
})
})
describe('with a resource with a malformed modified date', function () {
beforeEach(function () {
this.validResource.modified = 'not-a-date'
this.validRequest.compile.resources.push(this.validResource)
this.RequestParser.parse(this.validRequest, this.callback)
})
it('should return an error', function () {
this.callback
.calledWithMatch({
message:
'resource modified date could not be understood: ' +
this.validResource.modified,
})
.should.equal(true)
})
})
describe('with a valid buildId', function () {
beforeEach(function (done) {
this.validRequest.compile.options.buildId = '195a4869176-a4ad60bee7bf35e4'
this.RequestParser.parse(this.validRequest, (error, data) => {
if (error) return done(error)
this.data = data
done()
})
})
it('should return an error', function () {
this.data.buildId.should.equal('195a4869176-a4ad60bee7bf35e4')
})
})
describe('with a bad buildId', function () {
beforeEach(function () {
this.validRequest.compile.options.buildId = 'foo/bar'
this.RequestParser.parse(this.validRequest, this.callback)
})
it('should return an error', function () {
this.callback
.calledWithMatch({
message:
'buildId attribute does not match regex /^[0-9a-f]+-[0-9a-f]+$/',
})
.should.equal(true)
})
})
describe('with a resource with a valid date', function () {
beforeEach(function () {
this.date = '12:00 01/02/03'
this.validResource.modified = this.date
this.validRequest.compile.resources.push(this.validResource)
this.RequestParser.parse(this.validRequest, this.callback)
this.data = this.callback.args[0][1]
})
it('should return the date as a Javascript Date object', function () {
;(this.data.resources[0].modified instanceof Date).should.equal(true)
this.data.resources[0].modified
.getTime()
.should.equal(Date.parse(this.date))
})
})
describe('with a resource without either a content or URL attribute', function () {
beforeEach(function () {
delete this.validResource.url
delete this.validResource.content
this.validRequest.compile.resources.push(this.validResource)
this.RequestParser.parse(this.validRequest, this.callback)
})
it('should return an error', function () {
this.callback
.calledWithMatch({
message:
'all resources should have either a url or content attribute',
})
.should.equal(true)
})
})
describe('with a resource where the content is not a string', function () {
beforeEach(function () {
this.validResource.content = []
this.validRequest.compile.resources.push(this.validResource)
this.RequestParser.parse(this.validRequest, this.callback)
})
it('should return an error', function () {
this.callback
.calledWithMatch({ message: 'content attribute should be a string' })
.should.equal(true)
})
})
describe('with a resource where the url is not a string', function () {
beforeEach(function () {
this.validResource.url = []
this.validRequest.compile.resources.push(this.validResource)
this.RequestParser.parse(this.validRequest, this.callback)
})
it('should return an error', function () {
this.callback
.calledWithMatch({ message: 'url attribute should be a string' })
.should.equal(true)
})
})
describe('with a resource with a url', function () {
beforeEach(function () {
this.validResource.url = this.url = 'www.example.com'
this.validRequest.compile.resources.push(this.validResource)
this.RequestParser.parse(this.validRequest, this.callback)
this.data = this.callback.args[0][1]
})
it('should return the url in the parsed response', function () {
this.data.resources[0].url.should.equal(this.url)
})
})
describe('with a resource with a content attribute', function () {
beforeEach(function () {
this.validResource.content = this.content = 'Hello world'
this.validRequest.compile.resources.push(this.validResource)
this.RequestParser.parse(this.validRequest, this.callback)
this.data = this.callback.args[0][1]
})
it('should return the content in the parsed response', function () {
this.data.resources[0].content.should.equal(this.content)
})
})
describe('without a root resource path', function () {
beforeEach(function () {
delete this.validRequest.compile.rootResourcePath
this.RequestParser.parse(this.validRequest, this.callback)
this.data = this.callback.args[0][1]
})
it("should set the root resource path to 'main.tex' by default", function () {
this.data.rootResourcePath.should.equal('main.tex')
})
})
describe('with a root resource path', function () {
beforeEach(function () {
this.validRequest.compile.rootResourcePath = this.path = 'test.tex'
this.RequestParser.parse(this.validRequest, this.callback)
this.data = this.callback.args[0][1]
})
it('should return the root resource path in the parsed response', function () {
this.data.rootResourcePath.should.equal(this.path)
})
})
describe('with a root resource path that is not a string', function () {
beforeEach(function () {
this.validRequest.compile.rootResourcePath = []
this.RequestParser.parse(this.validRequest, this.callback)
})
it('should return an error', function () {
this.callback
.calledWithMatch({
message: 'rootResourcePath attribute should be a string',
})
.should.equal(true)
})
})
describe('with a root resource path that has a relative path', function () {
beforeEach(function () {
this.validRequest.compile.rootResourcePath = 'foo/../../bar.tex'
this.RequestParser.parse(this.validRequest, this.callback)
this.data = this.callback.args[0][1]
})
it('should return an error', function () {
this.callback
.calledWithMatch({ message: 'relative path in root resource' })
.should.equal(true)
})
})
describe('with a root resource path that has unescaped + relative path', function () {
beforeEach(function () {
this.validRequest.compile.rootResourcePath = 'foo/../bar.tex'
this.RequestParser.parse(this.validRequest, this.callback)
this.data = this.callback.args[0][1]
})
it('should return an error', function () {
this.callback
.calledWithMatch({ message: 'relative path in root resource' })
.should.equal(true)
})
})
describe('with an unknown syncType', function () {
beforeEach(function () {
this.validRequest.compile.options.syncType = 'unexpected'
this.RequestParser.parse(this.validRequest, this.callback)
this.data = this.callback.args[0][1]
})
it('should return an error', function () {
this.callback
.calledWithMatch({
message: 'syncType attribute should be one of: full, incremental',
})
.should.equal(true)
})
})
})

View File

@@ -0,0 +1,241 @@
/* eslint-disable
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/ResourceStateManager'
)
const Path = require('node:path')
const Errors = require('../../../app/js/Errors')
describe('ResourceStateManager', function () {
beforeEach(function () {
this.ResourceStateManager = SandboxedModule.require(modulePath, {
singleOnly: true,
requires: {
fs: (this.fs = {}),
'./SafeReader': (this.SafeReader = {}),
},
})
this.basePath = '/path/to/write/files/to'
this.resources = [
{ path: 'resource-1-mock' },
{ path: 'resource-2-mock' },
{ path: 'resource-3-mock' },
]
this.state = '1234567890'
this.resourceFileName = `${this.basePath}/.project-sync-state`
this.resourceFileContents = `${this.resources[0].path}\n${this.resources[1].path}\n${this.resources[2].path}\nstateHash:${this.state}`
return (this.callback = sinon.stub())
})
describe('saveProjectState', function () {
beforeEach(function () {
return (this.fs.writeFile = sinon.stub().callsArg(2))
})
describe('when the state is specified', function () {
beforeEach(function () {
return this.ResourceStateManager.saveProjectState(
this.state,
this.resources,
this.basePath,
this.callback
)
})
it('should write the resource list to disk', function () {
return this.fs.writeFile
.calledWith(this.resourceFileName, this.resourceFileContents)
.should.equal(true)
})
return it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
})
return describe('when the state is undefined', function () {
beforeEach(function () {
this.state = undefined
this.fs.unlink = sinon.stub().callsArg(1)
return this.ResourceStateManager.saveProjectState(
this.state,
this.resources,
this.basePath,
this.callback
)
})
it('should unlink the resource file', function () {
return this.fs.unlink
.calledWith(this.resourceFileName)
.should.equal(true)
})
it('should not write the resource list to disk', function () {
return this.fs.writeFile.called.should.equal(false)
})
return it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
})
})
describe('checkProjectStateMatches', function () {
describe('when the state matches', function () {
beforeEach(function () {
this.SafeReader.readFile = sinon
.stub()
.callsArgWith(3, null, this.resourceFileContents)
return this.ResourceStateManager.checkProjectStateMatches(
this.state,
this.basePath,
this.callback
)
})
it('should read the resource file', function () {
return this.SafeReader.readFile
.calledWith(this.resourceFileName)
.should.equal(true)
})
return it('should call the callback with the results', function () {
return this.callback
.calledWithMatch(null, this.resources)
.should.equal(true)
})
})
describe('when the state file is not present', function () {
beforeEach(function () {
this.SafeReader.readFile = sinon.stub().callsArg(3)
return this.ResourceStateManager.checkProjectStateMatches(
this.state,
this.basePath,
this.callback
)
})
it('should read the resource file', function () {
return this.SafeReader.readFile
.calledWith(this.resourceFileName)
.should.equal(true)
})
it('should call the callback with an error', function () {
this.callback
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
.should.equal(true)
const message = this.callback.args[0][0].message
expect(message).to.include('invalid state for incremental update')
})
})
return describe('when the state does not match', function () {
beforeEach(function () {
this.SafeReader.readFile = sinon
.stub()
.callsArgWith(3, null, this.resourceFileContents)
return this.ResourceStateManager.checkProjectStateMatches(
'not-the-original-state',
this.basePath,
this.callback
)
})
it('should call the callback with an error', function () {
this.callback
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
.should.equal(true)
const message = this.callback.args[0][0].message
expect(message).to.include('invalid state for incremental update')
})
})
})
return describe('checkResourceFiles', function () {
describe('when all the files are present', function () {
beforeEach(function () {
this.allFiles = [
this.resources[0].path,
this.resources[1].path,
this.resources[2].path,
]
return this.ResourceStateManager.checkResourceFiles(
this.resources,
this.allFiles,
this.basePath,
this.callback
)
})
return it('should call the callback', function () {
return this.callback.calledWithExactly().should.equal(true)
})
})
describe('when there is a missing file', function () {
beforeEach(function () {
this.allFiles = [this.resources[0].path, this.resources[1].path]
this.fs.stat = sinon.stub().callsArgWith(1, new Error())
return this.ResourceStateManager.checkResourceFiles(
this.resources,
this.allFiles,
this.basePath,
this.callback
)
})
it('should call the callback with an error', function () {
this.callback
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
.should.equal(true)
const message = this.callback.args[0][0].message
expect(message).to.include(
'resource files missing in incremental update'
)
})
})
return describe('when a resource contains a relative path', function () {
beforeEach(function () {
this.resources[0].path = '../foo/bar.tex'
this.allFiles = [
this.resources[0].path,
this.resources[1].path,
this.resources[2].path,
]
return this.ResourceStateManager.checkResourceFiles(
this.resources,
this.allFiles,
this.basePath,
this.callback
)
})
it('should call the callback with an error', function () {
this.callback.calledWith(sinon.match(Error)).should.equal(true)
const message = this.callback.args[0][0].message
expect(message).to.include('relative path in resource file list')
})
})
})
})

View File

@@ -0,0 +1,532 @@
/* eslint-disable
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/ResourceWriter'
)
const path = require('node:path')
describe('ResourceWriter', function () {
beforeEach(function () {
let Timer
this.ResourceWriter = SandboxedModule.require(modulePath, {
singleOnly: true,
requires: {
fs: (this.fs = {
mkdir: sinon.stub().callsArg(1),
unlink: sinon.stub().callsArg(1),
}),
'./ResourceStateManager': (this.ResourceStateManager = {}),
'./UrlCache': (this.UrlCache = {
createProjectDir: sinon.stub().yields(),
}),
'./OutputFileFinder': (this.OutputFileFinder = {}),
'./Metrics': (this.Metrics = {
inc: sinon.stub(),
Timer: (Timer = (function () {
Timer = class Timer {
static initClass() {
this.prototype.done = sinon.stub()
}
}
Timer.initClass()
return Timer
})()),
}),
},
})
this.project_id = 'project-id-123'
this.basePath = '/path/to/write/files/to'
return (this.callback = sinon.stub())
})
describe('syncResourcesToDisk on a full request', function () {
beforeEach(function () {
this.resources = ['resource-1-mock', 'resource-2-mock', 'resource-3-mock']
this.request = {
project_id: this.project_id,
syncState: (this.syncState = '0123456789abcdef'),
resources: this.resources,
}
this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
this.ResourceWriter._removeExtraneousFiles = sinon.stub().yields(null)
this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
return this.ResourceWriter.syncResourcesToDisk(
this.request,
this.basePath,
this.callback
)
})
it('should remove old files', function () {
return this.ResourceWriter._removeExtraneousFiles
.calledWith(this.request, this.resources, this.basePath)
.should.equal(true)
})
it('should write each resource to disk', function () {
return Array.from(this.resources).map(resource =>
this.ResourceWriter._writeResourceToDisk
.calledWith(this.project_id, resource, this.basePath)
.should.equal(true)
)
})
it('should store the sync state and resource list', function () {
return this.ResourceStateManager.saveProjectState
.calledWith(this.syncState, this.resources, this.basePath)
.should.equal(true)
})
return it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
})
describe('syncResourcesToDisk on an incremental update', function () {
beforeEach(function () {
this.resources = ['resource-1-mock']
this.request = {
project_id: this.project_id,
syncType: 'incremental',
syncState: (this.syncState = '1234567890abcdef'),
resources: this.resources,
}
this.fullResources = this.resources.concat(['file-1'])
this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
this.ResourceWriter._removeExtraneousFiles = sinon
.stub()
.yields(null, (this.outputFiles = []), (this.allFiles = []))
this.ResourceStateManager.checkProjectStateMatches = sinon
.stub()
.callsArgWith(2, null, this.fullResources)
this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
this.ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3)
return this.ResourceWriter.syncResourcesToDisk(
this.request,
this.basePath,
this.callback
)
})
it('should check the sync state matches', function () {
return this.ResourceStateManager.checkProjectStateMatches
.calledWith(this.syncState, this.basePath)
.should.equal(true)
})
it('should remove old files', function () {
return this.ResourceWriter._removeExtraneousFiles
.calledWith(this.request, this.fullResources, this.basePath)
.should.equal(true)
})
it('should check each resource exists', function () {
return this.ResourceStateManager.checkResourceFiles
.calledWith(this.fullResources, this.allFiles, this.basePath)
.should.equal(true)
})
it('should write each resource to disk', function () {
return Array.from(this.resources).map(resource =>
this.ResourceWriter._writeResourceToDisk
.calledWith(this.project_id, resource, this.basePath)
.should.equal(true)
)
})
return it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
})
describe('syncResourcesToDisk on an incremental update when the state does not match', function () {
beforeEach(function () {
this.resources = ['resource-1-mock']
this.request = {
project_id: this.project_id,
syncType: 'incremental',
syncState: (this.syncState = '1234567890abcdef'),
resources: this.resources,
}
this.ResourceStateManager.checkProjectStateMatches = sinon
.stub()
.callsArgWith(2, (this.error = new Error()))
return this.ResourceWriter.syncResourcesToDisk(
this.request,
this.basePath,
this.callback
)
})
it('should check whether the sync state matches', function () {
return this.ResourceStateManager.checkProjectStateMatches
.calledWith(this.syncState, this.basePath)
.should.equal(true)
})
return it('should call the callback with an error', function () {
return this.callback.calledWith(this.error).should.equal(true)
})
})
describe('_removeExtraneousFiles', function () {
beforeEach(function () {
this.output_files = [
{
path: 'output.pdf',
type: 'pdf',
},
{
path: 'extra/file.tex',
type: 'tex',
},
{
path: 'extra.aux',
type: 'aux',
},
{
path: 'cache/_chunk1',
},
{
path: 'figures/image-eps-converted-to.pdf',
type: 'pdf',
},
{
path: 'foo/main-figure0.md5',
type: 'md5',
},
{
path: 'foo/main-figure0.dpth',
type: 'dpth',
},
{
path: 'foo/main-figure0.pdf',
type: 'pdf',
},
{
path: '_minted-main/default-pyg-prefix.pygstyle',
type: 'pygstyle',
},
{
path: '_minted-main/default.pygstyle',
type: 'pygstyle',
},
{
path: '_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex',
type: 'pygtex',
},
{
path: '_markdown_main/30893013dec5d869a415610079774c2f.md.tex',
type: 'tex',
},
{
path: 'output.stdout',
},
{
path: 'output.stderr',
},
]
this.resources = 'mock-resources'
this.request = {
project_id: this.project_id,
syncType: 'incremental',
syncState: (this.syncState = '1234567890abcdef'),
resources: this.resources,
}
this.OutputFileFinder.findOutputFiles = sinon
.stub()
.callsArgWith(2, null, this.output_files)
this.ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1)
return this.ResourceWriter._removeExtraneousFiles(
this.request,
this.resources,
this.basePath,
this.callback
)
})
it('should find the existing output files', function () {
return this.OutputFileFinder.findOutputFiles
.calledWith(this.resources, this.basePath)
.should.equal(true)
})
it('should delete the output files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, 'output.pdf'))
.should.equal(true)
})
it('should delete the stdout log file', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, 'output.stdout'))
.should.equal(true)
})
it('should delete the stderr log file', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, 'output.stderr'))
.should.equal(true)
})
it('should delete the extra files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, 'extra/file.tex'))
.should.equal(true)
})
it('should not delete the extra aux files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, 'extra.aux'))
.should.equal(false)
})
it('should not delete the knitr cache file', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, 'cache/_chunk1'))
.should.equal(false)
})
it('should not delete the epstopdf converted files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(
path.join(this.basePath, 'figures/image-eps-converted-to.pdf')
)
.should.equal(false)
})
it('should not delete the tikz md5 files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, 'foo/main-figure0.md5'))
.should.equal(false)
})
it('should not delete the tikz dpth files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, 'foo/main-figure0.dpth'))
.should.equal(false)
})
it('should not delete the tikz pdf files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, 'foo/main-figure0.pdf'))
.should.equal(false)
})
it('should not delete the minted pygstyle files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(
path.join(this.basePath, '_minted-main/default-pyg-prefix.pygstyle')
)
.should.equal(false)
})
it('should not delete the minted default pygstyle files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(this.basePath, '_minted-main/default.pygstyle'))
.should.equal(false)
})
it('should not delete the minted default pygtex files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(
path.join(
this.basePath,
'_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex'
)
)
.should.equal(false)
})
it('should not delete the markdown md.tex files', function () {
return this.ResourceWriter._deleteFileIfNotDirectory
.calledWith(
path.join(
this.basePath,
'_markdown_main/30893013dec5d869a415610079774c2f.md.tex'
)
)
.should.equal(false)
})
it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
return it('should time the request', function () {
return this.Metrics.Timer.prototype.done.called.should.equal(true)
})
})
describe('_writeResourceToDisk', function () {
describe('with a url based resource', function () {
beforeEach(function () {
this.fs.mkdir = sinon.stub().callsArg(2)
this.resource = {
path: 'main.tex',
url: 'http://www.example.com/primary/main.tex',
fallbackURL: 'http://fallback.example.com/fallback/main.tex',
modified: Date.now(),
}
this.UrlCache.downloadUrlToFile = sinon
.stub()
.callsArgWith(5, 'fake error downloading file')
return this.ResourceWriter._writeResourceToDisk(
this.project_id,
this.resource,
this.basePath,
this.callback
)
})
it('should ensure the directory exists', function () {
this.fs.mkdir
.calledWith(
path.dirname(path.join(this.basePath, this.resource.path))
)
.should.equal(true)
})
it('should write the URL from the cache', function () {
return this.UrlCache.downloadUrlToFile
.calledWith(
this.project_id,
this.resource.url,
this.resource.fallbackURL,
path.join(this.basePath, this.resource.path),
this.resource.modified
)
.should.equal(true)
})
it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
return it('should not return an error if the resource writer errored', function () {
return expect(this.callback.args[0][0]).not.to.exist
})
})
describe('with a content based resource', function () {
beforeEach(function () {
this.resource = {
path: 'main.tex',
content: 'Hello world',
}
this.fs.writeFile = sinon.stub().callsArg(2)
this.fs.mkdir = sinon.stub().callsArg(2)
return this.ResourceWriter._writeResourceToDisk(
this.project_id,
this.resource,
this.basePath,
this.callback
)
})
it('should ensure the directory exists', function () {
return this.fs.mkdir
.calledWith(
path.dirname(path.join(this.basePath, this.resource.path))
)
.should.equal(true)
})
it('should write the contents to disk', function () {
return this.fs.writeFile
.calledWith(
path.join(this.basePath, this.resource.path),
this.resource.content
)
.should.equal(true)
})
return it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
})
return describe('with a file path that breaks out of the root folder', function () {
beforeEach(function () {
this.resource = {
path: '../../main.tex',
content: 'Hello world',
}
this.fs.writeFile = sinon.stub().callsArg(2)
return this.ResourceWriter._writeResourceToDisk(
this.project_id,
this.resource,
this.basePath,
this.callback
)
})
it('should not write to disk', function () {
return this.fs.writeFile.called.should.equal(false)
})
it('should return an error', function () {
this.callback.calledWith(sinon.match(Error)).should.equal(true)
const message = this.callback.args[0][0].message
expect(message).to.include('resource path is outside root directory')
})
})
})
return describe('checkPath', function () {
describe('with a valid path', function () {
beforeEach(function () {
return this.ResourceWriter.checkPath('foo', 'bar', this.callback)
})
return it('should return the joined path', function () {
return this.callback.calledWith(null, 'foo/bar').should.equal(true)
})
})
describe('with an invalid path', function () {
beforeEach(function () {
this.ResourceWriter.checkPath('foo', 'baz/../../bar', this.callback)
})
it('should return an error', function () {
this.callback.calledWith(sinon.match(Error)).should.equal(true)
const message = this.callback.args[0][0].message
expect(message).to.include('resource path is outside root directory')
})
})
describe('with another invalid path matching on a prefix', function () {
beforeEach(function () {
return this.ResourceWriter.checkPath(
'foo',
'../foobar/baz',
this.callback
)
})
it('should return an error', function () {
this.callback.calledWith(sinon.match(Error)).should.equal(true)
const message = this.callback.args[0][0].message
expect(message).to.include('resource path is outside root directory')
})
})
})
})

View File

@@ -0,0 +1,248 @@
/* eslint-disable
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const assert = require('node:assert')
const path = require('node:path')
const sinon = require('sinon')
const modulePath = path.join(
__dirname,
'../../../app/js/StaticServerForbidSymlinks'
)
const { expect } = require('chai')
describe('StaticServerForbidSymlinks', function () {
beforeEach(function () {
this.settings = {
path: {
compilesDir: '/compiles/here',
},
}
this.fs = {}
this.ForbidSymlinks = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': this.settings,
fs: this.fs,
},
})
this.dummyStatic = (rootDir, options) => (req, res, next) =>
// console.log "dummyStatic serving file", rootDir, "called with", req.url
// serve it
next()
this.StaticServerForbidSymlinks = this.ForbidSymlinks(
this.dummyStatic,
this.settings.path.compilesDir
)
this.req = {
params: {
project_id: '12345',
},
}
this.res = {}
return (this.req.url = '/12345/output.pdf')
})
describe('sending a normal file through', function () {
beforeEach(function () {
return (this.fs.realpath = sinon
.stub()
.callsArgWith(
1,
null,
`${this.settings.path.compilesDir}/${this.req.params.project_id}/output.pdf`
))
})
return it('should call next', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(200)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res, done)
})
})
describe('with a missing file', function () {
beforeEach(function () {
return (this.fs.realpath = sinon
.stub()
.callsArgWith(
1,
{ code: 'ENOENT' },
`${this.settings.path.compilesDir}/${this.req.params.project_id}/unknown.pdf`
))
})
return it('should send a 404', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(404)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res)
})
})
describe('with a new line', function () {
beforeEach(function () {
this.req.url = '/12345/output.pdf\nother file'
this.fs.realpath = sinon.stub().yields()
})
it('should process the correct file', function (done) {
this.res.sendStatus = () => {
this.fs.realpath.should.have.been.calledWith(
`${this.settings.path.compilesDir}/12345/output.pdf\nother file`
)
done()
}
this.StaticServerForbidSymlinks(this.req, this.res)
})
})
describe('with a symlink file', function () {
beforeEach(function () {
return (this.fs.realpath = sinon
.stub()
.callsArgWith(1, null, `/etc/${this.req.params.project_id}/output.pdf`))
})
return it('should send a 404', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(404)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res)
})
})
describe('with a relative file', function () {
beforeEach(function () {
return (this.req.url = '/12345/../67890/output.pdf')
})
return it('should send a 404', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(404)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res)
})
})
describe('with a unnormalized file containing .', function () {
beforeEach(function () {
return (this.req.url = '/12345/foo/./output.pdf')
})
return it('should send a 404', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(404)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res)
})
})
describe('with a file containing an empty path', function () {
beforeEach(function () {
return (this.req.url = '/12345/foo//output.pdf')
})
return it('should send a 404', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(404)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res)
})
})
describe('with a non-project file', function () {
beforeEach(function () {
return (this.req.url = '/.foo/output.pdf')
})
return it('should send a 404', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(404)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res)
})
})
describe('with a file outside the compiledir', function () {
beforeEach(function () {
return (this.req.url = '/../bar/output.pdf')
})
return it('should send a 404', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(404)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res)
})
})
describe('with a file with no leading /', function () {
beforeEach(function () {
return (this.req.url = './../bar/output.pdf')
})
return it('should send a 404', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(404)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res)
})
})
describe('with a github style path', function () {
beforeEach(function () {
this.req.url = '/henryoswald-latex_example/output/output.log'
return (this.fs.realpath = sinon
.stub()
.callsArgWith(
1,
null,
`${this.settings.path.compilesDir}/henryoswald-latex_example/output/output.log`
))
})
return it('should call next', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(200)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res, done)
})
})
return describe('with an error from fs.realpath', function () {
beforeEach(function () {
return (this.fs.realpath = sinon.stub().callsArgWith(1, 'error'))
})
return it('should send a 500', function (done) {
this.res.sendStatus = function (resCode) {
resCode.should.equal(500)
return done()
}
return this.StaticServerForbidSymlinks(this.req, this.res)
})
})
})

View File

@@ -0,0 +1,116 @@
const Path = require('node:path')
const SandboxedModule = require('sandboxed-module')
const { expect } = require('chai')
const MODULE_PATH = Path.join(__dirname, '../../../app/js/SynctexOutputParser')
describe('SynctexOutputParser', function () {
beforeEach(function () {
this.SynctexOutputParser = SandboxedModule.require(MODULE_PATH)
})
describe('parseViewOutput', function () {
it('parses valid output', function () {
const output = `This is SyncTeX command line utility, version 1.5
SyncTeX result begin
Output:/compile/output.pdf
Page:1
x:136.537964
y:661.437561
h:133.768356
v:663.928223
W:343.711060
H:9.962640
before:
offset:-1
middle:
after:
Output:/compile/output.pdf
Page:2
x:178.769592
y:649.482361
h:134.768356
v:651.973022
W:342.711060
H:19.962640
before:
offset:-1
middle:
after:
SyncTeX result end
`
const records = this.SynctexOutputParser.parseViewOutput(output)
expect(records).to.deep.equal([
{
page: 1,
h: 133.768356,
v: 663.928223,
width: 343.71106,
height: 9.96264,
},
{
page: 2,
h: 134.768356,
v: 651.973022,
width: 342.71106,
height: 19.96264,
},
])
})
it('handles garbage', function () {
const output = 'This computer is on strike!'
const records = this.SynctexOutputParser.parseViewOutput(output)
expect(records).to.deep.equal([])
})
})
describe('parseEditOutput', function () {
it('parses valid output', function () {
const output = `This is SyncTeX command line utility, version 1.5
SyncTeX result begin
Output:/compile/output.pdf
Input:/compile/main.tex
Line:17
Column:-1
Offset:0
Context:
SyncTeX result end
`
const records = this.SynctexOutputParser.parseEditOutput(
output,
'/compile'
)
expect(records).to.deep.equal([
{ file: 'main.tex', line: 17, column: -1 },
])
})
it('handles values that contain colons', function () {
const output = `This is SyncTeX command line utility, version 1.5
SyncTeX result begin
Output:/compile/output.pdf
Input:/compile/this-file:has-a-weird-name.tex
Line:17
Column:-1
Offset:0
Context:
SyncTeX result end
`
const records = this.SynctexOutputParser.parseEditOutput(
output,
'/compile'
)
expect(records).to.deep.equal([
{ file: 'this-file:has-a-weird-name.tex', line: 17, column: -1 },
])
})
it('handles garbage', function () {
const output = '2 + 2 = 4'
const records = this.SynctexOutputParser.parseEditOutput(output)
expect(records).to.deep.equal([])
})
})
})

View File

@@ -0,0 +1,185 @@
/* eslint-disable
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/TikzManager'
)
describe('TikzManager', function () {
beforeEach(function () {
return (this.TikzManager = SandboxedModule.require(modulePath, {
requires: {
'./ResourceWriter': (this.ResourceWriter = {}),
'./SafeReader': (this.SafeReader = {}),
fs: (this.fs = {}),
},
}))
})
describe('checkMainFile', function () {
beforeEach(function () {
this.compileDir = 'compile-dir'
this.mainFile = 'main.tex'
return (this.callback = sinon.stub())
})
describe('if there is already an output.tex file in the resources', function () {
beforeEach(function () {
this.resources = [{ path: 'main.tex' }, { path: 'output.tex' }]
return this.TikzManager.checkMainFile(
this.compileDir,
this.mainFile,
this.resources,
this.callback
)
})
return it('should call the callback with false ', function () {
return this.callback.calledWithExactly(null, false).should.equal(true)
})
})
return describe('if there is no output.tex file in the resources', function () {
beforeEach(function () {
this.resources = [{ path: 'main.tex' }]
return (this.ResourceWriter.checkPath = sinon
.stub()
.withArgs(this.compileDir, this.mainFile)
.callsArgWith(2, null, `${this.compileDir}/${this.mainFile}`))
})
describe('and the main file contains tikzexternalize', function () {
beforeEach(function () {
this.SafeReader.readFile = sinon
.stub()
.withArgs(`${this.compileDir}/${this.mainFile}`)
.callsArgWith(3, null, 'hello \\tikzexternalize')
return this.TikzManager.checkMainFile(
this.compileDir,
this.mainFile,
this.resources,
this.callback
)
})
it('should look at the file on disk', function () {
return this.SafeReader.readFile
.calledWith(`${this.compileDir}/${this.mainFile}`)
.should.equal(true)
})
return it('should call the callback with true ', function () {
return this.callback.calledWithExactly(null, true).should.equal(true)
})
})
describe('and the main file does not contain tikzexternalize', function () {
beforeEach(function () {
this.SafeReader.readFile = sinon
.stub()
.withArgs(`${this.compileDir}/${this.mainFile}`)
.callsArgWith(3, null, 'hello')
return this.TikzManager.checkMainFile(
this.compileDir,
this.mainFile,
this.resources,
this.callback
)
})
it('should look at the file on disk', function () {
return this.SafeReader.readFile
.calledWith(`${this.compileDir}/${this.mainFile}`)
.should.equal(true)
})
return it('should call the callback with false', function () {
return this.callback.calledWithExactly(null, false).should.equal(true)
})
})
return describe('and the main file contains \\usepackage{pstool}', function () {
beforeEach(function () {
this.SafeReader.readFile = sinon
.stub()
.withArgs(`${this.compileDir}/${this.mainFile}`)
.callsArgWith(3, null, 'hello \\usepackage[random-options]{pstool}')
return this.TikzManager.checkMainFile(
this.compileDir,
this.mainFile,
this.resources,
this.callback
)
})
it('should look at the file on disk', function () {
return this.SafeReader.readFile
.calledWith(`${this.compileDir}/${this.mainFile}`)
.should.equal(true)
})
return it('should call the callback with true ', function () {
return this.callback.calledWithExactly(null, true).should.equal(true)
})
})
})
})
return describe('injectOutputFile', function () {
beforeEach(function () {
this.rootDir = '/mock'
this.filename = 'filename.tex'
this.callback = sinon.stub()
this.content = `\
\\documentclass{article}
\\usepackage{tikz}
\\tikzexternalize
\\begin{document}
Hello world
\\end{document}\
`
this.fs.readFile = sinon.stub().callsArgWith(2, null, this.content)
this.fs.writeFile = sinon.stub().callsArg(3)
this.ResourceWriter.checkPath = sinon
.stub()
.callsArgWith(2, null, `${this.rootDir}/${this.filename}`)
return this.TikzManager.injectOutputFile(
this.rootDir,
this.filename,
this.callback
)
})
it('sould check the path', function () {
return this.ResourceWriter.checkPath
.calledWith(this.rootDir, this.filename)
.should.equal(true)
})
it('should read the file', function () {
return this.fs.readFile
.calledWith(`${this.rootDir}/${this.filename}`, 'utf8')
.should.equal(true)
})
it('should write out the same file as output.tex', function () {
return this.fs.writeFile
.calledWith(`${this.rootDir}/output.tex`, this.content, { flag: 'wx' })
.should.equal(true)
})
return it('should call the callback', function () {
return this.callback.called.should.equal(true)
})
})
})

View File

@@ -0,0 +1,148 @@
/* eslint-disable
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = require('node:path').join(
__dirname,
'../../../app/js/UrlCache'
)
describe('UrlCache', function () {
beforeEach(function () {
this.callback = sinon.stub()
this.url =
'http://filestore/project/60b0dd39c418bc00598a0d22/file/60ae721ffb1d920027d3201f'
this.fallbackURL = 'http://filestore/bucket/project-blobs/key/ab/cd/ef'
this.project_id = '60b0dd39c418bc00598a0d22'
return (this.UrlCache = SandboxedModule.require(modulePath, {
requires: {
'./UrlFetcher': (this.UrlFetcher = {
promises: { pipeUrlToFileWithRetry: sinon.stub().resolves() },
}),
'@overleaf/settings': (this.Settings = {
path: { clsiCacheDir: '/cache/dir' },
}),
'@overleaf/metrics': {
Timer: sinon.stub().returns({ done: sinon.stub() }),
},
fs: (this.fs = {
promises: {
rm: sinon.stub().resolves(),
copyFile: sinon.stub().resolves(),
},
}),
},
}))
})
describe('downloadUrlToFile', function () {
beforeEach(function () {
this.destPath = 'path/to/destination'
})
it('should not download on the happy path', function (done) {
this.UrlCache.downloadUrlToFile(
this.project_id,
this.url,
this.fallbackURL,
this.destPath,
this.lastModified,
error => {
expect(error).to.not.exist
expect(
this.UrlFetcher.promises.pipeUrlToFileWithRetry.called
).to.equal(false)
done()
}
)
})
it('should not download on the semi-happy path', function (done) {
const codedError = new Error()
codedError.code = 'ENOENT'
this.fs.promises.copyFile.onCall(0).rejects(codedError)
this.fs.promises.copyFile.onCall(1).resolves()
this.UrlCache.downloadUrlToFile(
this.project_id,
this.url,
this.fallbackURL,
this.destPath,
this.lastModified,
error => {
expect(error).to.not.exist
expect(
this.UrlFetcher.promises.pipeUrlToFileWithRetry.called
).to.equal(false)
done()
}
)
})
it('should download on cache miss', function (done) {
const codedError = new Error()
codedError.code = 'ENOENT'
this.fs.promises.copyFile.onCall(0).rejects(codedError)
this.fs.promises.copyFile.onCall(1).rejects(codedError)
this.fs.promises.copyFile.onCall(2).resolves()
this.UrlCache.downloadUrlToFile(
this.project_id,
this.url,
this.fallbackURL,
this.destPath,
this.lastModified,
error => {
expect(error).to.not.exist
expect(
this.UrlFetcher.promises.pipeUrlToFileWithRetry.called
).to.equal(true)
done()
}
)
})
it('should raise non cache-miss errors', function (done) {
const codedError = new Error()
codedError.code = 'FOO'
this.fs.promises.copyFile.rejects(codedError)
this.UrlCache.downloadUrlToFile(
this.project_id,
this.url,
this.fallbackURL,
this.destPath,
this.lastModified,
error => {
expect(error).to.equal(codedError)
done()
}
)
})
})
describe('clearProject', function () {
beforeEach(function (done) {
this.UrlCache.clearProject(this.project_id, done)
})
it('should clear the cache in bulk', function () {
expect(
this.fs.promises.rm.calledWith('/cache/dir/' + this.project_id, {
force: true,
recursive: true,
})
).to.equal(true)
})
})
})

View File

@@ -0,0 +1,93 @@
const fs = require('node:fs')
const Path = require('node:path')
const { expect } = require('chai')
const { parseXrefTable } = require('../../../app/js/XrefParser')
const { NoXrefTableError } = require('../../../app/js/Errors')
const PATH_EXAMPLES = 'test/acceptance/fixtures/examples/'
const PATH_SNAPSHOTS = 'test/unit/js/snapshots/pdfjs/'
const EXAMPLES = fs.readdirSync(PATH_EXAMPLES)
function snapshotPath(example) {
return Path.join(PATH_SNAPSHOTS, example, 'XrefTable.json')
}
function pdfPath(example) {
return Path.join(PATH_EXAMPLES, example, 'output.pdf')
}
async function loadContext(example) {
const size = (await fs.promises.stat(pdfPath(example))).size
let blob
try {
blob = await fs.promises.readFile(snapshotPath(example))
} catch (e) {
if (e.code !== 'ENOENT') {
throw e
}
}
const snapshot = blob ? JSON.parse(blob) : null
return {
size,
snapshot,
}
}
async function backFillSnapshot(example, size) {
const table = await parseXrefTable(pdfPath(example), size, () => {})
await fs.promises.mkdir(Path.dirname(snapshotPath(example)), {
recursive: true,
})
await fs.promises.writeFile(
snapshotPath(example),
JSON.stringify(table, null, 2)
)
return table
}
describe('pdfjs', function () {
describe('when the pdf is an empty file', function () {
it('should yield no entries', async function () {
const path = 'does/not/matter.pdf'
let table
try {
table = await parseXrefTable(path, 0)
} catch (e) {
expect(e).to.be.an.instanceof(NoXrefTableError)
}
expect(table).to.not.exist
})
})
for (const example of EXAMPLES) {
describe(example, function () {
let size, snapshot
before('load snapshot', async function () {
const ctx = await loadContext(example)
size = ctx.size
snapshot = ctx.snapshot
})
before('back fill new snapshot', async function () {
if (snapshot === null) {
console.error('back filling snapshot for', example)
snapshot = await backFillSnapshot(example, size)
}
})
it('should produce the expected xRef table', async function () {
const table = await parseXrefTable(pdfPath(example), size, () => {})
// compare the essential parts of the xref table only
expect(table.xRefEntries[0]).to.include({ offset: 0 })
expect(table.xRefEntries.slice(1)).to.deep.equal(
snapshot.xRefEntries
.slice(1)
.filter(xref => xref.uncompressed) // we only use the uncompressed fields
.map(xref => {
return { offset: xref.offset, uncompressed: xref.uncompressed } // ignore unused gen field
})
)
})
})
}
})

View File

@@ -0,0 +1,7 @@
obj
<< /Type /ObjStm /Length 447 /Filter /FlateDecode /N 5 /First 32 >>
stream
x<EFBFBD><EFBFBD>RQk<EFBFBD>0<>ǍK'ɒ <0C><>$<24>vkGIJ[(y<><12><18>8<EFBFBD>*<2A><><EFBFBD><EFBFBD>$<24>I<EFBFBD>R<EFBFBD>d<EFBFBD><64><EFBFBD>><3E>I"H@<05><06><>9@J!` V<>/gg f<>>BZx<5A>J<EFBFBD><4A><EFBFBD>9<EFBFBD>ۮ]-B<>'ZNg <20><>k<EFBFBD>%<25>i\<5C>!<11><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>f<EFBFBD><66>4<EFBFBD><34><EFBFBD><EFBFBD><6D><DD81><EFBFBD>4<EFBFBD><34><EFBFBD><39>CY]\<5C><06>@<40><><EFBFBD><EFBFBD><EFBFBD>!<21> 4c<34>
U<EFBFBD><EFBFBD>f<1D>=<17><>JgO<67><17><11>g<EFBFBD><67><EFBFBD>><3E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>zz<>>A)<29><>C9W<39>w<EFBFBD>K<EFBFBD>q<EFBFBD>PÜ#<23><><EFBFBD><EFBFBD>/48<34><38>VX/ <0B>T<EFBFBD><54><EFBFBD>p <09>-%2"<22><>*<2A>B;<3B><>X<EFBFBD><0E><>2<EFBFBD>,9<><39>G<03>z;<3B><>E<EFBFBD>Υ<1B><>Jj/<2F>c
<EFBFBD>n<EFBFBD>%<25><><EFBFBD><EFBFBD><18><><EFBFBD>ᵦf3<66><33><EFBFBD>]!<21><><EFBFBD><EFBFBD><EFBFBD>=<3D>y<EFBFBD><79>
,<2C>s]<5D><>@<40>e+COW.C<><43><EFBFBD><EFBFBD><09>c_ťX<C5A5> v<><15>><3E>N<EFBFBD><4E><06><><EFBFBD><EFBFBD>2u<32><75>7=<3D><><EFBFBD><EFBFBD><EFBFBD>}<7D><> #<23><>HV<48>r<EFBFBD>9?kv<01><>6<EFBFBD><36><EFBFBD><EFBFBD><EFBFBD>G<EFBFBD>^<5E>z׬.<2E><1B><>v<11><>=Uyj<79><13><>ǡp<C7A1><70><EFBFBD><EFBFBD>z<><7A>2

View File

@@ -0,0 +1,359 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 123103,
"gen": 0,
"uncompressed": true
},
{
"offset": 123422,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 1084,
"gen": 0,
"uncompressed": true
},
{
"offset": 1244,
"gen": 0,
"uncompressed": true
},
{
"offset": 4001,
"gen": 0,
"uncompressed": true
},
{
"offset": 4155,
"gen": 0,
"uncompressed": true
},
{
"offset": 4297,
"gen": 0,
"uncompressed": true
},
{
"offset": 4933,
"gen": 0,
"uncompressed": true
},
{
"offset": 5309,
"gen": 0,
"uncompressed": true
},
{
"offset": 5498,
"gen": 0,
"uncompressed": true
},
{
"offset": 30250,
"gen": 0,
"uncompressed": true
},
{
"offset": 31471,
"gen": 0,
"uncompressed": true
},
{
"offset": 38404,
"gen": 0,
"uncompressed": true
},
{
"offset": 39046,
"gen": 0,
"uncompressed": true
},
{
"offset": 40166,
"gen": 0,
"uncompressed": true
},
{
"offset": 40906,
"gen": 0,
"uncompressed": true
},
{
"offset": 65560,
"gen": 0,
"uncompressed": true
},
{
"offset": 74702,
"gen": 0,
"uncompressed": true
},
{
"offset": 81705,
"gen": 0,
"uncompressed": true
},
{
"offset": 97182,
"gen": 0,
"uncompressed": true
},
{
"offset": 104117,
"gen": 0,
"uncompressed": true
},
{
"offset": 111195,
"gen": 0,
"uncompressed": true
},
{
"offset": 118571,
"gen": 0,
"uncompressed": true
},
{
"offset": 6,
"gen": 0
},
{
"offset": 6,
"gen": 1
},
{
"offset": 6,
"gen": 2
},
{
"offset": 6,
"gen": 3
},
{
"offset": 6,
"gen": 4
},
{
"offset": 6,
"gen": 5
},
{
"offset": 6,
"gen": 6
},
{
"offset": 6,
"gen": 7
},
{
"offset": 6,
"gen": 8
},
{
"offset": 6,
"gen": 9
},
{
"offset": 6,
"gen": 10
},
{
"offset": 6,
"gen": 11
},
{
"offset": 6,
"gen": 12
},
{
"offset": 6,
"gen": 13
},
{
"offset": 6,
"gen": 14
},
{
"offset": 6,
"gen": 15
},
{
"offset": 6,
"gen": 16
},
{
"offset": 6,
"gen": 17
},
{
"offset": 6,
"gen": 18
},
{
"offset": 6,
"gen": 19
},
{
"offset": 6,
"gen": 20
},
{
"offset": 6,
"gen": 21
},
{
"offset": 6,
"gen": 22
},
{
"offset": 6,
"gen": 23
},
{
"offset": 6,
"gen": 24
},
{
"offset": 6,
"gen": 25
},
{
"offset": 6,
"gen": 26
},
{
"offset": 6,
"gen": 27
},
{
"offset": 6,
"gen": 28
},
{
"offset": 6,
"gen": 29
},
{
"offset": 6,
"gen": 30
},
{
"offset": 6,
"gen": 31
},
{
"offset": 6,
"gen": 32
},
{
"offset": 6,
"gen": 33
},
{
"offset": 6,
"gen": 34
},
{
"offset": 6,
"gen": 35
},
{
"offset": 6,
"gen": 36
},
{
"offset": 6,
"gen": 37
},
{
"offset": 6,
"gen": 38
},
{
"offset": 6,
"gen": 39
},
{
"offset": 6,
"gen": 40
},
{
"offset": 6,
"gen": 41
},
{
"offset": 6,
"gen": 42
},
{
"offset": 6,
"gen": 43
},
{
"offset": 6,
"gen": 44
},
{
"offset": 6,
"gen": 45
},
{
"offset": 6,
"gen": 46
},
{
"offset": 6,
"gen": 47
},
{
"offset": 6,
"gen": 48
},
{
"offset": 6,
"gen": 49
},
{
"offset": 6,
"gen": 50
},
{
"offset": 6,
"gen": 51
},
{
"offset": 6,
"gen": 52
},
{
"offset": 6,
"gen": 53
},
{
"offset": 6,
"gen": 54
},
{
"offset": 6,
"gen": 55
}
],
"startXRefTable": 123422
}

View File

@@ -0,0 +1,148 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 59313,
"gen": 0,
"uncompressed": true
},
{
"offset": 59561,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 734,
"gen": 0,
"uncompressed": true
},
{
"offset": 784,
"gen": 0,
"uncompressed": true
},
{
"offset": 913,
"gen": 0,
"uncompressed": true
},
{
"offset": 1028,
"gen": 0,
"uncompressed": true
},
{
"offset": 1528,
"gen": 0,
"uncompressed": true
},
{
"offset": 9787,
"gen": 0,
"uncompressed": true
},
{
"offset": 18282,
"gen": 0,
"uncompressed": true
},
{
"offset": 33607,
"gen": 0,
"uncompressed": true
},
{
"offset": 45579,
"gen": 0,
"uncompressed": true
},
{
"offset": 58005,
"gen": 0,
"uncompressed": true
},
{
"offset": 14,
"gen": 0
},
{
"offset": 14,
"gen": 1
},
{
"offset": 14,
"gen": 2
},
{
"offset": 14,
"gen": 3
},
{
"offset": 14,
"gen": 4
},
{
"offset": 14,
"gen": 5
},
{
"offset": 14,
"gen": 6
},
{
"offset": 14,
"gen": 7
},
{
"offset": 14,
"gen": 8
},
{
"offset": 14,
"gen": 9
},
{
"offset": 14,
"gen": 10
},
{
"offset": 14,
"gen": 11
},
{
"offset": 14,
"gen": 12
},
{
"offset": 14,
"gen": 13
},
{
"offset": 14,
"gen": 14
},
{
"offset": 14,
"gen": 15
},
{
"offset": 14,
"gen": 16
}
],
"startXRefTable": 59561
}

View File

@@ -0,0 +1,182 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 67338,
"gen": 0,
"uncompressed": true
},
{
"offset": 67606,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 790,
"gen": 0,
"uncompressed": true
},
{
"offset": 840,
"gen": 0,
"uncompressed": true
},
{
"offset": 975,
"gen": 0,
"uncompressed": true
},
{
"offset": 1083,
"gen": 0,
"uncompressed": true
},
{
"offset": 1578,
"gen": 0,
"uncompressed": true
},
{
"offset": 9881,
"gen": 0,
"uncompressed": true
},
{
"offset": 17868,
"gen": 0,
"uncompressed": true
},
{
"offset": 29906,
"gen": 0,
"uncompressed": true
},
{
"offset": 38400,
"gen": 0,
"uncompressed": true
},
{
"offset": 46656,
"gen": 0,
"uncompressed": true
},
{
"offset": 56198,
"gen": 0,
"uncompressed": true
},
{
"offset": 65682,
"gen": 0,
"uncompressed": true
},
{
"offset": 16,
"gen": 0
},
{
"offset": 16,
"gen": 1
},
{
"offset": 16,
"gen": 2
},
{
"offset": 16,
"gen": 3
},
{
"offset": 16,
"gen": 4
},
{
"offset": 16,
"gen": 5
},
{
"offset": 16,
"gen": 6
},
{
"offset": 16,
"gen": 7
},
{
"offset": 16,
"gen": 8
},
{
"offset": 16,
"gen": 9
},
{
"offset": 16,
"gen": 10
},
{
"offset": 16,
"gen": 11
},
{
"offset": 16,
"gen": 12
},
{
"offset": 16,
"gen": 13
},
{
"offset": 16,
"gen": 14
},
{
"offset": 16,
"gen": 15
},
{
"offset": 16,
"gen": 16
},
{
"offset": 16,
"gen": 17
},
{
"offset": 16,
"gen": 18
},
{
"offset": 16,
"gen": 19
},
{
"offset": 16,
"gen": 20
},
{
"offset": 16,
"gen": 21
},
{
"offset": 16,
"gen": 22
}
],
"startXRefTable": 67606
}

View File

@@ -0,0 +1,226 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 69708,
"gen": 0,
"uncompressed": true
},
{
"offset": 70038,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 867,
"gen": 0,
"uncompressed": true
},
{
"offset": 990,
"gen": 0,
"uncompressed": true
},
{
"offset": 1143,
"gen": 0,
"uncompressed": true
},
{
"offset": 1251,
"gen": 0,
"uncompressed": true
},
{
"offset": 1834,
"gen": 0,
"uncompressed": true
},
{
"offset": 10137,
"gen": 0,
"uncompressed": true
},
{
"offset": 18124,
"gen": 0,
"uncompressed": true
},
{
"offset": 31939,
"gen": 0,
"uncompressed": true
},
{
"offset": 40433,
"gen": 0,
"uncompressed": true
},
{
"offset": 48689,
"gen": 0,
"uncompressed": true
},
{
"offset": 58231,
"gen": 0,
"uncompressed": true
},
{
"offset": 67715,
"gen": 0,
"uncompressed": true
},
{
"offset": 16,
"gen": 0
},
{
"offset": 16,
"gen": 1
},
{
"offset": 16,
"gen": 2
},
{
"offset": 16,
"gen": 3
},
{
"offset": 16,
"gen": 4
},
{
"offset": 16,
"gen": 5
},
{
"offset": 16,
"gen": 6
},
{
"offset": 16,
"gen": 7
},
{
"offset": 16,
"gen": 8
},
{
"offset": 16,
"gen": 9
},
{
"offset": 16,
"gen": 10
},
{
"offset": 16,
"gen": 11
},
{
"offset": 16,
"gen": 12
},
{
"offset": 16,
"gen": 13
},
{
"offset": 16,
"gen": 14
},
{
"offset": 16,
"gen": 15
},
{
"offset": 16,
"gen": 16
},
{
"offset": 16,
"gen": 17
},
{
"offset": 16,
"gen": 18
},
{
"offset": 16,
"gen": 19
},
{
"offset": 16,
"gen": 20
},
{
"offset": 16,
"gen": 21
},
{
"offset": 16,
"gen": 22
},
{
"offset": 16,
"gen": 23
},
{
"offset": 16,
"gen": 24
},
{
"offset": 16,
"gen": 25
},
{
"offset": 16,
"gen": 26
},
{
"offset": 16,
"gen": 27
},
{
"offset": 16,
"gen": 28
},
{
"offset": 16,
"gen": 29
},
{
"offset": 16,
"gen": 30
},
{
"offset": 16,
"gen": 31
},
{
"offset": 16,
"gen": 32
},
{
"offset": 16,
"gen": 33
}
],
"startXRefTable": 70038
}

View File

@@ -0,0 +1,145 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 31354,
"gen": 0,
"uncompressed": true
},
{
"offset": 31614,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 727,
"gen": 0,
"uncompressed": true
},
{
"offset": 777,
"gen": 0,
"uncompressed": true
},
{
"offset": 909,
"gen": 0,
"uncompressed": true
},
{
"offset": 1017,
"gen": 0,
"uncompressed": true
},
{
"offset": 19127,
"gen": 0,
"uncompressed": true
},
{
"offset": 19313,
"gen": 0,
"uncompressed": true
},
{
"offset": 19557,
"gen": 0,
"uncompressed": true
},
{
"offset": 19948,
"gen": 0,
"uncompressed": true
},
{
"offset": 20677,
"gen": 0,
"uncompressed": true
},
{
"offset": 23321,
"gen": 0,
"uncompressed": true
},
{
"offset": 30318,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0
},
{
"offset": 15,
"gen": 1
},
{
"offset": 15,
"gen": 2
},
{
"offset": 15,
"gen": 3
},
{
"offset": 15,
"gen": 4
},
{
"offset": 15,
"gen": 5
},
{
"offset": 15,
"gen": 6
},
{
"offset": 15,
"gen": 7
},
{
"offset": 15,
"gen": 8
},
{
"offset": 15,
"gen": 9
},
{
"offset": 15,
"gen": 10
},
{
"offset": 15,
"gen": 11
},
{
"offset": 15,
"gen": 12
},
{
"offset": 15,
"gen": 13
},
{
"offset": 15,
"gen": 14
}
],
"startXRefTable": 31614
}

View File

@@ -0,0 +1,135 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 27064,
"gen": 0,
"uncompressed": true
},
{
"offset": 27312,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 713,
"gen": 0,
"uncompressed": true
},
{
"offset": 763,
"gen": 0,
"uncompressed": true
},
{
"offset": 892,
"gen": 0,
"uncompressed": true
},
{
"offset": 1007,
"gen": 0,
"uncompressed": true
},
{
"offset": 1235,
"gen": 0,
"uncompressed": true
},
{
"offset": 4832,
"gen": 0,
"uncompressed": true
},
{
"offset": 12199,
"gen": 0,
"uncompressed": true
},
{
"offset": 19196,
"gen": 0,
"uncompressed": true
},
{
"offset": 26341,
"gen": 0,
"uncompressed": true
},
{
"offset": 13,
"gen": 0
},
{
"offset": 13,
"gen": 1
},
{
"offset": 13,
"gen": 2
},
{
"offset": 13,
"gen": 3
},
{
"offset": 13,
"gen": 4
},
{
"offset": 13,
"gen": 5
},
{
"offset": 13,
"gen": 6
},
{
"offset": 13,
"gen": 7
},
{
"offset": 13,
"gen": 8
},
{
"offset": 13,
"gen": 9
},
{
"offset": 13,
"gen": 10
},
{
"offset": 13,
"gen": 11
},
{
"offset": 13,
"gen": 12
},
{
"offset": 13,
"gen": 13
},
{
"offset": 13,
"gen": 14
}
],
"startXRefTable": 27312
}

View File

@@ -0,0 +1,110 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 65535,
"free": true
},
{
"offset": 4964,
"gen": 0,
"uncompressed": true
},
{
"offset": 5023,
"gen": 0,
"uncompressed": true
},
{
"offset": 5234,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 734,
"gen": 0,
"uncompressed": true
},
{
"offset": 799,
"gen": 0,
"uncompressed": true
},
{
"offset": 933,
"gen": 0,
"uncompressed": true
},
{
"offset": 1104,
"gen": 0,
"uncompressed": true
},
{
"offset": 1947,
"gen": 0,
"uncompressed": true
},
{
"offset": 1992,
"gen": 0,
"uncompressed": true
},
{
"offset": 2182,
"gen": 0,
"uncompressed": true
},
{
"offset": 2427,
"gen": 0,
"uncompressed": true
},
{
"offset": 2597,
"gen": 0,
"uncompressed": true
},
{
"offset": 2822,
"gen": 0,
"uncompressed": true
},
{
"offset": 2989,
"gen": 0,
"uncompressed": true
},
{
"offset": 3239,
"gen": 0,
"uncompressed": true
},
{
"offset": 3271,
"gen": 0,
"uncompressed": true
},
{
"offset": 3328,
"gen": 0,
"uncompressed": true
},
{
"offset": 3740,
"gen": 0,
"uncompressed": true
},
{
"offset": 4270,
"gen": 0,
"uncompressed": true
}
],
"startXRefTable": 6682
}

View File

@@ -0,0 +1,113 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 31058,
"gen": 0,
"uncompressed": true
},
{
"offset": 31307,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 678,
"gen": 0,
"uncompressed": true
},
{
"offset": 728,
"gen": 0,
"uncompressed": true
},
{
"offset": 855,
"gen": 0,
"uncompressed": true
},
{
"offset": 970,
"gen": 0,
"uncompressed": true
},
{
"offset": 1203,
"gen": 0,
"uncompressed": true
},
{
"offset": 18852,
"gen": 0,
"uncompressed": true
},
{
"offset": 30165,
"gen": 0,
"uncompressed": true
},
{
"offset": 11,
"gen": 0
},
{
"offset": 11,
"gen": 1
},
{
"offset": 11,
"gen": 2
},
{
"offset": 11,
"gen": 3
},
{
"offset": 11,
"gen": 4
},
{
"offset": 11,
"gen": 5
},
{
"offset": 11,
"gen": 6
},
{
"offset": 11,
"gen": 7
},
{
"offset": 11,
"gen": 8
},
{
"offset": 11,
"gen": 9
},
{
"offset": 11,
"gen": 10
},
{
"offset": 11,
"gen": 11
}
],
"startXRefTable": 31307
}

View File

@@ -0,0 +1,129 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 6344,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 707,
"gen": 0,
"uncompressed": true
},
{
"offset": 757,
"gen": 0,
"uncompressed": true
},
{
"offset": 887,
"gen": 0,
"uncompressed": true
},
{
"offset": 990,
"gen": 0,
"uncompressed": true
},
{
"offset": 1257,
"gen": 0,
"uncompressed": true
},
{
"offset": 1679,
"gen": 0,
"uncompressed": true
},
{
"offset": 2052,
"gen": 0,
"uncompressed": true
},
{
"offset": 4249,
"gen": 0,
"uncompressed": true
},
{
"offset": 4343,
"gen": 0,
"uncompressed": true
},
{
"offset": 5387,
"gen": 0,
"uncompressed": true
},
{
"offset": 5481,
"gen": 0,
"uncompressed": true
},
{
"offset": 5519,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0
},
{
"offset": 15,
"gen": 1
},
{
"offset": 15,
"gen": 2
},
{
"offset": 15,
"gen": 3
},
{
"offset": 15,
"gen": 4
},
{
"offset": 15,
"gen": 5
},
{
"offset": 15,
"gen": 6
},
{
"offset": 15,
"gen": 7
},
{
"offset": 15,
"gen": 8
},
{
"offset": 15,
"gen": 9
},
{
"offset": 15,
"gen": 10
}
],
"startXRefTable": 6344
}

View File

@@ -0,0 +1,114 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 34767,
"gen": 0,
"uncompressed": true
},
{
"offset": 35015,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 678,
"gen": 0,
"uncompressed": true
},
{
"offset": 728,
"gen": 0,
"uncompressed": true
},
{
"offset": 856,
"gen": 0,
"uncompressed": true
},
{
"offset": 971,
"gen": 0,
"uncompressed": true
},
{
"offset": 1394,
"gen": 0,
"uncompressed": true
},
{
"offset": 10990,
"gen": 0,
"uncompressed": true
},
{
"offset": 19087,
"gen": 0,
"uncompressed": true
},
{
"offset": 33769,
"gen": 0,
"uncompressed": true
},
{
"offset": 12,
"gen": 0
},
{
"offset": 12,
"gen": 1
},
{
"offset": 12,
"gen": 2
},
{
"offset": 12,
"gen": 3
},
{
"offset": 12,
"gen": 4
},
{
"offset": 12,
"gen": 5
},
{
"offset": 12,
"gen": 6
},
{
"offset": 12,
"gen": 7
},
{
"offset": 12,
"gen": 8
},
{
"offset": 12,
"gen": 9
},
{
"offset": 12,
"gen": 10
}
],
"startXRefTable": 35015
}

View File

@@ -0,0 +1,109 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 23295,
"gen": 0,
"uncompressed": true
},
{
"offset": 23543,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 671,
"gen": 0,
"uncompressed": true
},
{
"offset": 721,
"gen": 0,
"uncompressed": true
},
{
"offset": 847,
"gen": 0,
"uncompressed": true
},
{
"offset": 955,
"gen": 0,
"uncompressed": true
},
{
"offset": 7385,
"gen": 0,
"uncompressed": true
},
{
"offset": 15752,
"gen": 0,
"uncompressed": true
},
{
"offset": 22721,
"gen": 0,
"uncompressed": true
},
{
"offset": 11,
"gen": 0
},
{
"offset": 11,
"gen": 1
},
{
"offset": 11,
"gen": 2
},
{
"offset": 11,
"gen": 3
},
{
"offset": 11,
"gen": 4
},
{
"offset": 11,
"gen": 5
},
{
"offset": 11,
"gen": 6
},
{
"offset": 11,
"gen": 7
},
{
"offset": 11,
"gen": 8
},
{
"offset": 11,
"gen": 9
},
{
"offset": 11,
"gen": 10
}
],
"startXRefTable": 23543
}

View File

@@ -0,0 +1,101 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 24490,
"gen": 0,
"uncompressed": true
},
{
"offset": 24739,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 657,
"gen": 0,
"uncompressed": true
},
{
"offset": 707,
"gen": 0,
"uncompressed": true
},
{
"offset": 833,
"gen": 0,
"uncompressed": true
},
{
"offset": 948,
"gen": 0,
"uncompressed": true
},
{
"offset": 1290,
"gen": 0,
"uncompressed": true
},
{
"offset": 13083,
"gen": 0,
"uncompressed": true
},
{
"offset": 23411,
"gen": 0,
"uncompressed": true
},
{
"offset": 11,
"gen": 0
},
{
"offset": 11,
"gen": 1
},
{
"offset": 11,
"gen": 2
},
{
"offset": 11,
"gen": 3
},
{
"offset": 11,
"gen": 4
},
{
"offset": 11,
"gen": 5
},
{
"offset": 11,
"gen": 6
},
{
"offset": 11,
"gen": 7
},
{
"offset": 11,
"gen": 8
}
],
"startXRefTable": 24739
}

View File

@@ -0,0 +1,148 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 43550,
"gen": 0,
"uncompressed": true
},
{
"offset": 43799,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 734,
"gen": 0,
"uncompressed": true
},
{
"offset": 784,
"gen": 0,
"uncompressed": true
},
{
"offset": 913,
"gen": 0,
"uncompressed": true
},
{
"offset": 1021,
"gen": 0,
"uncompressed": true
},
{
"offset": 1546,
"gen": 0,
"uncompressed": true
},
{
"offset": 5794,
"gen": 0,
"uncompressed": true
},
{
"offset": 12915,
"gen": 0,
"uncompressed": true
},
{
"offset": 23660,
"gen": 0,
"uncompressed": true
},
{
"offset": 30657,
"gen": 0,
"uncompressed": true
},
{
"offset": 42604,
"gen": 0,
"uncompressed": true
},
{
"offset": 14,
"gen": 0
},
{
"offset": 14,
"gen": 1
},
{
"offset": 14,
"gen": 2
},
{
"offset": 14,
"gen": 3
},
{
"offset": 14,
"gen": 4
},
{
"offset": 14,
"gen": 5
},
{
"offset": 14,
"gen": 6
},
{
"offset": 14,
"gen": 7
},
{
"offset": 14,
"gen": 8
},
{
"offset": 14,
"gen": 9
},
{
"offset": 14,
"gen": 10
},
{
"offset": 14,
"gen": 11
},
{
"offset": 14,
"gen": 12
},
{
"offset": 14,
"gen": 13
},
{
"offset": 14,
"gen": 14
},
{
"offset": 14,
"gen": 15
},
{
"offset": 14,
"gen": 16
}
],
"startXRefTable": 43799
}

View File

@@ -0,0 +1,182 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 75299,
"gen": 0,
"uncompressed": true
},
{
"offset": 75548,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 790,
"gen": 0,
"uncompressed": true
},
{
"offset": 840,
"gen": 0,
"uncompressed": true
},
{
"offset": 975,
"gen": 0,
"uncompressed": true
},
{
"offset": 1083,
"gen": 0,
"uncompressed": true
},
{
"offset": 2128,
"gen": 0,
"uncompressed": true
},
{
"offset": 13799,
"gen": 0,
"uncompressed": true
},
{
"offset": 23682,
"gen": 0,
"uncompressed": true
},
{
"offset": 31867,
"gen": 0,
"uncompressed": true
},
{
"offset": 36116,
"gen": 0,
"uncompressed": true
},
{
"offset": 50352,
"gen": 0,
"uncompressed": true
},
{
"offset": 61569,
"gen": 0,
"uncompressed": true
},
{
"offset": 73516,
"gen": 0,
"uncompressed": true
},
{
"offset": 16,
"gen": 0
},
{
"offset": 16,
"gen": 1
},
{
"offset": 16,
"gen": 2
},
{
"offset": 16,
"gen": 3
},
{
"offset": 16,
"gen": 4
},
{
"offset": 16,
"gen": 5
},
{
"offset": 16,
"gen": 6
},
{
"offset": 16,
"gen": 7
},
{
"offset": 16,
"gen": 8
},
{
"offset": 16,
"gen": 9
},
{
"offset": 16,
"gen": 10
},
{
"offset": 16,
"gen": 11
},
{
"offset": 16,
"gen": 12
},
{
"offset": 16,
"gen": 13
},
{
"offset": 16,
"gen": 14
},
{
"offset": 16,
"gen": 15
},
{
"offset": 16,
"gen": 16
},
{
"offset": 16,
"gen": 17
},
{
"offset": 16,
"gen": 18
},
{
"offset": 16,
"gen": 19
},
{
"offset": 16,
"gen": 20
},
{
"offset": 16,
"gen": 21
},
{
"offset": 16,
"gen": 22
}
],
"startXRefTable": 75548
}

View File

@@ -0,0 +1,140 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 65535,
"free": true
},
{
"offset": 25097,
"gen": 0,
"uncompressed": true
},
{
"offset": 25156,
"gen": 0,
"uncompressed": true
},
{
"offset": 25367,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 854,
"gen": 0,
"uncompressed": true
},
{
"offset": 919,
"gen": 0,
"uncompressed": true
},
{
"offset": 1074,
"gen": 0,
"uncompressed": true
},
{
"offset": 1245,
"gen": 0,
"uncompressed": true
},
{
"offset": 18343,
"gen": 0,
"uncompressed": true
},
{
"offset": 18388,
"gen": 0,
"uncompressed": true
},
{
"offset": 18752,
"gen": 0,
"uncompressed": true
},
{
"offset": 19071,
"gen": 0,
"uncompressed": true
},
{
"offset": 19360,
"gen": 0,
"uncompressed": true
},
{
"offset": 19604,
"gen": 0,
"uncompressed": true
},
{
"offset": 19770,
"gen": 0,
"uncompressed": true
},
{
"offset": 20007,
"gen": 0,
"uncompressed": true
},
{
"offset": 20174,
"gen": 0,
"uncompressed": true
},
{
"offset": 20424,
"gen": 0,
"uncompressed": true
},
{
"offset": 20456,
"gen": 0,
"uncompressed": true
},
{
"offset": 20525,
"gen": 0,
"uncompressed": true
},
{
"offset": 23109,
"gen": 0,
"uncompressed": true
},
{
"offset": 23500,
"gen": 0,
"uncompressed": true
},
{
"offset": 24229,
"gen": 0,
"uncompressed": true
},
{
"offset": 24641,
"gen": 0,
"uncompressed": true
},
{
"offset": 24741,
"gen": 0,
"uncompressed": true
},
{
"offset": 24985,
"gen": 0,
"uncompressed": true
}
],
"startXRefTable": 26815
}

View File

@@ -0,0 +1,94 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 3568,
"gen": 0,
"uncompressed": true
},
{
"offset": 3777,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 643,
"gen": 0,
"uncompressed": true
},
{
"offset": 693,
"gen": 0,
"uncompressed": true
},
{
"offset": 819,
"gen": 0,
"uncompressed": true
},
{
"offset": 934,
"gen": 0,
"uncompressed": true
},
{
"offset": 1118,
"gen": 0,
"uncompressed": true
},
{
"offset": 1210,
"gen": 0,
"uncompressed": true
},
{
"offset": 2555,
"gen": 0,
"uncompressed": true
},
{
"offset": 3030,
"gen": 0,
"uncompressed": true
},
{
"offset": 12,
"gen": 0
},
{
"offset": 12,
"gen": 1
},
{
"offset": 12,
"gen": 2
},
{
"offset": 12,
"gen": 3
},
{
"offset": 12,
"gen": 4
},
{
"offset": 12,
"gen": 5
}
],
"startXRefTable": 3777
}

View File

@@ -0,0 +1,128 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 16762,
"gen": 0,
"uncompressed": true
},
{
"offset": 16877,
"gen": 0,
"uncompressed": true
},
{
"offset": 17142,
"gen": 0,
"uncompressed": true
},
{
"offset": 24335,
"gen": 0,
"uncompressed": true
},
{
"offset": 32164,
"gen": 0,
"uncompressed": true
},
{
"offset": 32412,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 671,
"gen": 0,
"uncompressed": true
},
{
"offset": 721,
"gen": 0,
"uncompressed": true
},
{
"offset": 856,
"gen": 0,
"uncompressed": true
},
{
"offset": 973,
"gen": 0,
"uncompressed": true
},
{
"offset": 1318,
"gen": 0,
"uncompressed": true
},
{
"offset": 2218,
"gen": 0,
"uncompressed": true
},
{
"offset": 13,
"gen": 0
},
{
"offset": 13,
"gen": 1
},
{
"offset": 13,
"gen": 2
},
{
"offset": 13,
"gen": 3
},
{
"offset": 13,
"gen": 4
},
{
"offset": 13,
"gen": 5
},
{
"offset": 13,
"gen": 6
},
{
"offset": 13,
"gen": 7
},
{
"offset": 13,
"gen": 8
},
{
"offset": 13,
"gen": 9
},
{
"offset": 13,
"gen": 10
},
{
"offset": 13,
"gen": 11
}
],
"startXRefTable": 32412
}

View File

@@ -0,0 +1,111 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 16778,
"gen": 0,
"uncompressed": true
},
{
"offset": 16893,
"gen": 0,
"uncompressed": true
},
{
"offset": 17109,
"gen": 0,
"uncompressed": true
},
{
"offset": 24938,
"gen": 0,
"uncompressed": true
},
{
"offset": 25186,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 650,
"gen": 0,
"uncompressed": true
},
{
"offset": 700,
"gen": 0,
"uncompressed": true
},
{
"offset": 836,
"gen": 0,
"uncompressed": true
},
{
"offset": 953,
"gen": 0,
"uncompressed": true
},
{
"offset": 1298,
"gen": 0,
"uncompressed": true
},
{
"offset": 2103,
"gen": 0,
"uncompressed": true
},
{
"offset": 12,
"gen": 0
},
{
"offset": 12,
"gen": 1
},
{
"offset": 12,
"gen": 2
},
{
"offset": 12,
"gen": 3
},
{
"offset": 12,
"gen": 4
},
{
"offset": 12,
"gen": 5
},
{
"offset": 12,
"gen": 6
},
{
"offset": 12,
"gen": 7
},
{
"offset": 12,
"gen": 8
}
],
"startXRefTable": 25186
}

View File

@@ -0,0 +1,97 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 20679,
"gen": 0,
"uncompressed": true
},
{
"offset": 20927,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 650,
"gen": 0,
"uncompressed": true
},
{
"offset": 700,
"gen": 0,
"uncompressed": true
},
{
"offset": 826,
"gen": 0,
"uncompressed": true
},
{
"offset": 934,
"gen": 0,
"uncompressed": true
},
{
"offset": 1252,
"gen": 0,
"uncompressed": true
},
{
"offset": 8248,
"gen": 0,
"uncompressed": true
},
{
"offset": 20115,
"gen": 0,
"uncompressed": true
},
{
"offset": 11,
"gen": 0
},
{
"offset": 11,
"gen": 1
},
{
"offset": 11,
"gen": 2
},
{
"offset": 11,
"gen": 3
},
{
"offset": 11,
"gen": 4
},
{
"offset": 11,
"gen": 5
},
{
"offset": 11,
"gen": 6
},
{
"offset": 11,
"gen": 7
}
],
"startXRefTable": 20927
}

View File

@@ -0,0 +1,156 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 29506,
"gen": 0,
"uncompressed": true
},
{
"offset": 29621,
"gen": 0,
"uncompressed": true
},
{
"offset": 29918,
"gen": 0,
"uncompressed": true
},
{
"offset": 30033,
"gen": 0,
"uncompressed": true
},
{
"offset": 30274,
"gen": 0,
"uncompressed": true
},
{
"offset": 30389,
"gen": 0,
"uncompressed": true
},
{
"offset": 30644,
"gen": 0,
"uncompressed": true
},
{
"offset": 42802,
"gen": 0,
"uncompressed": true
},
{
"offset": 43050,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 695,
"gen": 0,
"uncompressed": true
},
{
"offset": 746,
"gen": 0,
"uncompressed": true
},
{
"offset": 900,
"gen": 0,
"uncompressed": true
},
{
"offset": 1017,
"gen": 0,
"uncompressed": true
},
{
"offset": 1286,
"gen": 0,
"uncompressed": true
},
{
"offset": 2443,
"gen": 0,
"uncompressed": true
},
{
"offset": 13147,
"gen": 0,
"uncompressed": true
},
{
"offset": 16,
"gen": 0
},
{
"offset": 16,
"gen": 1
},
{
"offset": 16,
"gen": 2
},
{
"offset": 16,
"gen": 3
},
{
"offset": 16,
"gen": 4
},
{
"offset": 16,
"gen": 5
},
{
"offset": 16,
"gen": 6
},
{
"offset": 16,
"gen": 7
},
{
"offset": 16,
"gen": 8
},
{
"offset": 16,
"gen": 9
},
{
"offset": 16,
"gen": 10
},
{
"offset": 16,
"gen": 11
},
{
"offset": 16,
"gen": 12
},
{
"offset": 16,
"gen": 13
}
],
"startXRefTable": 43050
}

View File

@@ -0,0 +1,114 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 34102,
"gen": 0,
"uncompressed": true
},
{
"offset": 34350,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 678,
"gen": 0,
"uncompressed": true
},
{
"offset": 728,
"gen": 0,
"uncompressed": true
},
{
"offset": 856,
"gen": 0,
"uncompressed": true
},
{
"offset": 971,
"gen": 0,
"uncompressed": true
},
{
"offset": 1514,
"gen": 0,
"uncompressed": true
},
{
"offset": 10973,
"gen": 0,
"uncompressed": true
},
{
"offset": 19139,
"gen": 0,
"uncompressed": true
},
{
"offset": 33047,
"gen": 0,
"uncompressed": true
},
{
"offset": 12,
"gen": 0
},
{
"offset": 12,
"gen": 1
},
{
"offset": 12,
"gen": 2
},
{
"offset": 12,
"gen": 3
},
{
"offset": 12,
"gen": 4
},
{
"offset": 12,
"gen": 5
},
{
"offset": 12,
"gen": 6
},
{
"offset": 12,
"gen": 7
},
{
"offset": 12,
"gen": 8
},
{
"offset": 12,
"gen": 9
},
{
"offset": 12,
"gen": 10
}
],
"startXRefTable": 34350
}

View File

@@ -0,0 +1,111 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 9449,
"gen": 0,
"uncompressed": true
},
{
"offset": 9564,
"gen": 0,
"uncompressed": true
},
{
"offset": 9730,
"gen": 0,
"uncompressed": true
},
{
"offset": 17293,
"gen": 0,
"uncompressed": true
},
{
"offset": 17541,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 650,
"gen": 0,
"uncompressed": true
},
{
"offset": 700,
"gen": 0,
"uncompressed": true
},
{
"offset": 835,
"gen": 0,
"uncompressed": true
},
{
"offset": 952,
"gen": 0,
"uncompressed": true
},
{
"offset": 1097,
"gen": 0,
"uncompressed": true
},
{
"offset": 1758,
"gen": 0,
"uncompressed": true
},
{
"offset": 12,
"gen": 0
},
{
"offset": 12,
"gen": 1
},
{
"offset": 12,
"gen": 2
},
{
"offset": 12,
"gen": 3
},
{
"offset": 12,
"gen": 4
},
{
"offset": 12,
"gen": 5
},
{
"offset": 12,
"gen": 6
},
{
"offset": 12,
"gen": 7
},
{
"offset": 12,
"gen": 8
}
],
"startXRefTable": 17541
}

View File

@@ -0,0 +1,114 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 37282,
"gen": 0,
"uncompressed": true
},
{
"offset": 37530,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 678,
"gen": 0,
"uncompressed": true
},
{
"offset": 728,
"gen": 0,
"uncompressed": true
},
{
"offset": 856,
"gen": 0,
"uncompressed": true
},
{
"offset": 971,
"gen": 0,
"uncompressed": true
},
{
"offset": 1322,
"gen": 0,
"uncompressed": true
},
{
"offset": 9581,
"gen": 0,
"uncompressed": true
},
{
"offset": 24286,
"gen": 0,
"uncompressed": true
},
{
"offset": 36258,
"gen": 0,
"uncompressed": true
},
{
"offset": 12,
"gen": 0
},
{
"offset": 12,
"gen": 1
},
{
"offset": 12,
"gen": 2
},
{
"offset": 12,
"gen": 3
},
{
"offset": 12,
"gen": 4
},
{
"offset": 12,
"gen": 5
},
{
"offset": 12,
"gen": 6
},
{
"offset": 12,
"gen": 7
},
{
"offset": 12,
"gen": 8
},
{
"offset": 12,
"gen": 9
},
{
"offset": 12,
"gen": 10
}
],
"startXRefTable": 37530
}

View File

@@ -0,0 +1,128 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 48194,
"gen": 0,
"uncompressed": true
},
{
"offset": 48442,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 699,
"gen": 0,
"uncompressed": true
},
{
"offset": 749,
"gen": 0,
"uncompressed": true
},
{
"offset": 878,
"gen": 0,
"uncompressed": true
},
{
"offset": 1000,
"gen": 0,
"uncompressed": true
},
{
"offset": 8546,
"gen": 0,
"uncompressed": true
},
{
"offset": 9072,
"gen": 0,
"uncompressed": true
},
{
"offset": 10659,
"gen": 0,
"uncompressed": true
},
{
"offset": 18919,
"gen": 0,
"uncompressed": true
},
{
"offset": 35129,
"gen": 0,
"uncompressed": true
},
{
"offset": 47101,
"gen": 0,
"uncompressed": true
},
{
"offset": 14,
"gen": 0
},
{
"offset": 14,
"gen": 1
},
{
"offset": 14,
"gen": 2
},
{
"offset": 14,
"gen": 3
},
{
"offset": 14,
"gen": 4
},
{
"offset": 14,
"gen": 5
},
{
"offset": 14,
"gen": 6
},
{
"offset": 14,
"gen": 7
},
{
"offset": 14,
"gen": 8
},
{
"offset": 14,
"gen": 9
},
{
"offset": 14,
"gen": 10
},
{
"offset": 14,
"gen": 11
}
],
"startXRefTable": 48442
}

View File

@@ -0,0 +1,168 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 2924,
"gen": 0,
"uncompressed": true
},
{
"offset": 3039,
"gen": 0,
"uncompressed": true
},
{
"offset": 4606,
"gen": 0,
"uncompressed": true
},
{
"offset": 4721,
"gen": 0,
"uncompressed": true
},
{
"offset": 7754,
"gen": 0,
"uncompressed": true
},
{
"offset": 7870,
"gen": 0,
"uncompressed": true
},
{
"offset": 11668,
"gen": 0,
"uncompressed": true
},
{
"offset": 21077,
"gen": 0,
"uncompressed": true
},
{
"offset": 28498,
"gen": 0,
"uncompressed": true
},
{
"offset": 35464,
"gen": 0,
"uncompressed": true
},
{
"offset": 35699,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 703,
"gen": 0,
"uncompressed": true
},
{
"offset": 754,
"gen": 0,
"uncompressed": true
},
{
"offset": 909,
"gen": 0,
"uncompressed": true
},
{
"offset": 1026,
"gen": 0,
"uncompressed": true
},
{
"offset": 2161,
"gen": 0,
"uncompressed": true
},
{
"offset": 18,
"gen": 0
},
{
"offset": 18,
"gen": 1
},
{
"offset": 18,
"gen": 2
},
{
"offset": 18,
"gen": 3
},
{
"offset": 18,
"gen": 4
},
{
"offset": 18,
"gen": 5
},
{
"offset": 18,
"gen": 6
},
{
"offset": 18,
"gen": 7
},
{
"offset": 18,
"gen": 8
},
{
"offset": 18,
"gen": 9
},
{
"offset": 18,
"gen": 10
},
{
"offset": 18,
"gen": 11
},
{
"offset": 18,
"gen": 12
},
{
"offset": 18,
"gen": 13
},
{
"offset": 18,
"gen": 14
},
{
"offset": 18,
"gen": 15
},
{
"offset": 18,
"gen": 16
}
],
"startXRefTable": 35699
}

View File

@@ -0,0 +1,98 @@
{
"xRefEntries": [
{
"offset": 0,
"gen": 0,
"free": true
},
{
"offset": 8578,
"gen": 0,
"uncompressed": true
},
{
"offset": 15,
"gen": 0,
"uncompressed": true
},
{
"offset": 216,
"gen": 0,
"uncompressed": true
},
{
"offset": 658,
"gen": 0,
"uncompressed": true
},
{
"offset": 708,
"gen": 0,
"uncompressed": true
},
{
"offset": 837,
"gen": 0,
"uncompressed": true
},
{
"offset": 940,
"gen": 0,
"uncompressed": true
},
{
"offset": 1191,
"gen": 0,
"uncompressed": true
},
{
"offset": 1627,
"gen": 0,
"uncompressed": true
},
{
"offset": 7676,
"gen": 0,
"uncompressed": true
},
{
"offset": 7784,
"gen": 0,
"uncompressed": true
},
{
"offset": 7822,
"gen": 0,
"uncompressed": true
},
{
"offset": 12,
"gen": 0
},
{
"offset": 12,
"gen": 1
},
{
"offset": 12,
"gen": 2
},
{
"offset": 12,
"gen": 3
},
{
"offset": 12,
"gen": 4
},
{
"offset": 12,
"gen": 5
},
{
"offset": 12,
"gen": 6
}
],
"startXRefTable": 8578
}