Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions services/tangled/tangled-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const description = `
[Tangled](https://tangled.org) does not expose license metadata via its AT Protocol record,
so this badge fetches the project's LICENSE file from the default branch and matches it
against well-known license texts to derive an SPDX id.
`

function httpErrorsFor(notFoundMessage = 'repo not found') {
return { 404: notFoundMessage }
}

// Order matters: more-specific patterns must precede subsets.
const LICENSE_PATTERNS = [
{ spdx: 'AGPL-3.0', re: /GNU AFFERO GENERAL PUBLIC LICENSE\s+Version 3/i },
{ spdx: 'GPL-3.0', re: /GNU GENERAL PUBLIC LICENSE\s+Version 3/i },
{ spdx: 'GPL-2.0', re: /GNU GENERAL PUBLIC LICENSE\s+Version 2/i },
{ spdx: 'LGPL-3.0', re: /GNU LESSER GENERAL PUBLIC LICENSE\s+Version 3/i },
{ spdx: 'LGPL-2.1', re: /GNU LESSER GENERAL PUBLIC LICENSE\s+Version 2\.1/i },
{ spdx: 'Apache-2.0', re: /Apache License,?\s+Version 2\.0/i },
{ spdx: 'MPL-2.0', re: /Mozilla Public License Version 2\.0/i },
{ spdx: 'BSL-1.0', re: /Boost Software License - Version 1\.0/i },
{ spdx: 'EPL-2.0', re: /Eclipse Public License - v 2\.0/i },
{
spdx: 'BSD-3-Clause',
re: /Redistribution and use in source and binary forms[\s\S]+Neither the name of[\s\S]+nor the names/i,
},
{
spdx: 'BSD-2-Clause',
re: /Redistribution and use in source and binary forms/i,
},
// ISC before 0BSD: ISC requires the copyright notice, 0BSD does not.
{
spdx: 'ISC',
re: /Permission to use, copy, modify, and\/or distribute this software[\s\S]+provided that the above copyright notice/i,
},
{
spdx: '0BSD',
re: /Permission to use, copy, modify, and\/or distribute this software/i,
},
{
spdx: 'MIT',
re: /Permission is hereby granted, free of charge, to any person obtaining a copy/i,
},
{ spdx: 'WTFPL', re: /DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE/i },
{
spdx: 'Unlicense',
re: /This is free and unencumbered software released into the public domain/i,
},
{ spdx: 'CC0-1.0', re: /Creative Commons (?:CC0|Zero) 1\.0/i },
]

const SCAN_BYTES = 4096

function detectSpdxId(content) {
const head = content.slice(0, SCAN_BYTES)
for (const { spdx, re } of LICENSE_PATTERNS) {
if (re.test(head)) return spdx
}
return undefined
}

export { description, httpErrorsFor, detectSpdxId }
55 changes: 55 additions & 0 deletions services/tangled/tangled-license.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { BaseService, NotFound, pathParams } from '../index.js'
import { renderLicenseBadge } from '../licenses.js'
import { description, detectSpdxId, httpErrorsFor } from './tangled-helper.js'

const CANDIDATE_PATHS = [
'LICENSE',
'LICENSE.md',
'LICENSE.txt',
'COPYING',
'COPYING.md',
]

export default class TangledLicense extends BaseService {
static category = 'license'
static route = { base: 'tangled/l', pattern: ':owner/:repo' }
static openApi = {
'/tangled/l/{owner}/{repo}': {
get: {
summary: 'Tangled License',
description,
parameters: pathParams(
{ name: 'owner', example: 'tangled.org' },
{ name: 'repo', example: 'core' },
),
},
},
}

static defaultBadgeData = { label: 'license' }

static render({ license }) {
if (license === undefined) return { message: 'unknown' }
if (license === null) return { message: 'not specified' }
return renderLicenseBadge({ license })
}

async handle({ owner, repo }) {
const httpErrors = httpErrorsFor('repo not found')
for (const path of CANDIDATE_PATHS) {
try {
const { buffer } = await this._request({
url: `https://tangled.org/${owner}/${repo}/raw/HEAD/${path}`,
httpErrors,
})
return this.constructor.render({
license: detectSpdxId(buffer.toString()),
})
} catch (e) {
if (e instanceof NotFound) continue
throw e
}
}
return this.constructor.render({ license: null })
}
}
67 changes: 67 additions & 0 deletions services/tangled/tangled-license.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { test, given } from 'sazerac'
import { detectSpdxId } from './tangled-helper.js'
import TangledLicense from './tangled-license.service.js'

const MIT_TEXT = `MIT License

Copyright (c) 2024 Example

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction,`

const MIT_TEXT_ALT_YEAR = `MIT License

Copyright (c) 2019 Another Author

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction,`

const APACHE_TEXT = ` Apache License
Version 2.0, January 2004
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION`

const GPL3_TEXT = ` GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007`

const BSD3_TEXT = `Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.`

const BSD2_TEXT = `Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.`

test(TangledLicense.render, () => {
given({ license: undefined }).expect({ message: 'unknown' })
given({ license: null }).expect({ message: 'not specified' })
given({ license: 'MIT' }).expect({ message: 'MIT', color: 'green' })
given({ license: 'Apache-2.0' }).expect({
message: 'Apache-2.0',
color: 'green',
})
given({ license: 'GPL-3.0' }).expect({ message: 'GPL-3.0', color: 'orange' })
})

test(detectSpdxId, () => {
given(MIT_TEXT).expect('MIT')
given(MIT_TEXT_ALT_YEAR).expect('MIT')
given(APACHE_TEXT).expect('Apache-2.0')
given(GPL3_TEXT).expect('GPL-3.0')
given(BSD3_TEXT).expect('BSD-3-Clause')
given(BSD2_TEXT).expect('BSD-2-Clause')
given('This is a proprietary software license.').expect(undefined)
})
97 changes: 97 additions & 0 deletions services/tangled/tangled-license.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { licenseToColor } from '../licenses.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()

const mitColor = licenseToColor('MIT')
const apacheColor = licenseToColor('Apache-2.0')
const gplColor = licenseToColor('GPL-3.0')

const MIT_BODY = `MIT License

Copyright (c) 2024 Example

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction,`

const APACHE_BODY = ` Apache License, Version 2.0
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION`
const GPL3_BODY = ` GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007`

t.create('MIT license')
.get('/testowner/testrepo.json')
.intercept(nock =>
nock('https://tangled.org')
.get('/testowner/testrepo/raw/HEAD/LICENSE')
.reply(200, MIT_BODY),
)
.expectBadge({ label: 'license', message: 'MIT', color: mitColor })

t.create('Apache-2.0 license')
.get('/testowner/testrepo.json')
.intercept(nock =>
nock('https://tangled.org')
.get('/testowner/testrepo/raw/HEAD/LICENSE')
.reply(200, APACHE_BODY),
)
.expectBadge({ label: 'license', message: 'Apache-2.0', color: apacheColor })

t.create('GPL-3.0 license')
.get('/testowner/testrepo.json')
.intercept(nock =>
nock('https://tangled.org')
.get('/testowner/testrepo/raw/HEAD/LICENSE')
.reply(200, GPL3_BODY),
)
.expectBadge({ label: 'license', message: 'GPL-3.0', color: gplColor })

t.create('unrecognised license body')
.get('/testowner/testrepo.json')
.intercept(nock =>
nock('https://tangled.org')
.get('/testowner/testrepo/raw/HEAD/LICENSE')
.reply(200, 'All rights reserved. Proprietary and confidential.'),
)
.expectBadge({ label: 'license', message: 'unknown', color: 'lightgrey' })

t.create('falls through to LICENSE.md when LICENSE is absent')
.get('/testowner/testrepo.json')
.intercept(nock =>
nock('https://tangled.org')
.get('/testowner/testrepo/raw/HEAD/LICENSE')
.reply(404)
.get('/testowner/testrepo/raw/HEAD/LICENSE.md')
.reply(200, MIT_BODY),
)
.expectBadge({ label: 'license', message: 'MIT', color: mitColor })

t.create('no license file found')
.get('/testowner/nolicense.json')
.intercept(nock =>
nock('https://tangled.org')
.get('/testowner/nolicense/raw/HEAD/LICENSE')
.reply(404)
.get('/testowner/nolicense/raw/HEAD/LICENSE.md')
.reply(404)
.get('/testowner/nolicense/raw/HEAD/LICENSE.txt')
.reply(404)
.get('/testowner/nolicense/raw/HEAD/COPYING')
.reply(404)
.get('/testowner/nolicense/raw/HEAD/COPYING.md')
.reply(404),
)
.expectBadge({
label: 'license',
message: 'not specified',
color: 'lightgrey',
})

t.create('dotted owner handle (e.g. icyphox.sh)')
.get('/icyphox.sh/testrepo.json')
.intercept(nock =>
nock('https://tangled.org')
.get('/icyphox.sh/testrepo/raw/HEAD/LICENSE')
.reply(200, MIT_BODY),
)
.expectBadge({ label: 'license', message: 'MIT', color: mitColor })
Loading