Version 4.6.0
This commit is contained in:
1 parent
f79dcf067a
commit
517a5e1f70
2036 files changed
+110041
-26162
No files matched your search
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief URL Safe Base64 Encoding Polyfill
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 11 Feb 2021
|
||||
*/
|
||||
|
||||
namespace Base64Url;
|
||||
|
||||
class Base64Url
|
||||
{
|
||||
public static function encode( string $string ): string
|
||||
{
|
||||
return \str_replace( '=', '', \strtr( base64_encode( $string ), '+/', '-_' ) );
|
||||
}
|
||||
|
||||
public static function decode( string $string ): string
|
||||
{
|
||||
return base64_decode( \strtr( $string, '-_', '+/' ) );
|
||||
}
|
||||
}
|
||||
Vendored
+409
@@ -0,0 +1,409 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.9.2](https://github.com/brick/math/releases/tag/0.9.2) - 2021-01-20
|
||||
|
||||
🐛 **Bug fix**
|
||||
|
||||
- Incorrect results could be returned when using the BCMath calculator, with a default scale set with `bcscale()`, on PHP >= 7.2 (#55).
|
||||
|
||||
## [0.9.1](https://github.com/brick/math/releases/tag/0.9.1) - 2020-08-19
|
||||
|
||||
✨ New features
|
||||
|
||||
- `BigInteger::not()` returns the bitwise `NOT` value
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
|
||||
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
|
||||
|
||||
## [0.9.0](https://github.com/brick/math/releases/tag/0.9.0) - 2020-08-18
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `BigNumber::of()` now accepts `.123` and `123.` formats, both of which return a `BigDecimal`
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Deprecated method `BigInteger::powerMod()` has been removed - use `modPow()` instead
|
||||
- Deprecated method `BigInteger::parse()` has been removed - use `fromBase()` instead
|
||||
|
||||
## [0.8.17](https://github.com/brick/math/releases/tag/0.8.17) - 2020-08-19
|
||||
|
||||
🐛 **Bug fix**
|
||||
|
||||
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
|
||||
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
|
||||
|
||||
## [0.8.16](https://github.com/brick/math/releases/tag/0.8.16) - 2020-08-18
|
||||
|
||||
🚑 **Critical fix**
|
||||
|
||||
- This version reintroduces the deprecated `BigInteger::parse()` method, that has been removed by mistake in version `0.8.9` and should have lasted for the whole `0.8` release cycle.
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::modInverse()` calculates a modular multiplicative inverse
|
||||
- `BigInteger::fromBytes()` creates a `BigInteger` from a byte string
|
||||
- `BigInteger::toBytes()` converts a `BigInteger` to a byte string
|
||||
- `BigInteger::randomBits()` creates a pseudo-random `BigInteger` of a given bit length
|
||||
- `BigInteger::randomRange()` creates a pseudo-random `BigInteger` between two bounds
|
||||
|
||||
💩 **Deprecations**
|
||||
|
||||
- `BigInteger::powerMod()` is now deprecated in favour of `modPow()`
|
||||
|
||||
## [0.8.15](https://github.com/brick/math/releases/tag/0.8.15) - 2020-04-15
|
||||
|
||||
🐛 **Fixes**
|
||||
|
||||
- added missing `ext-json` requirement, due to `BigNumber` implementing `JsonSerializable`
|
||||
|
||||
⚡️ **Optimizations**
|
||||
|
||||
- additional optimization in `BigInteger::remainder()`
|
||||
|
||||
## [0.8.14](https://github.com/brick/math/releases/tag/0.8.14) - 2020-02-18
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::getLowestSetBit()` returns the index of the rightmost one bit
|
||||
|
||||
## [0.8.13](https://github.com/brick/math/releases/tag/0.8.13) - 2020-02-16
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::isEven()` tests whether the number is even
|
||||
- `BigInteger::isOdd()` tests whether the number is odd
|
||||
- `BigInteger::testBit()` tests if a bit is set
|
||||
- `BigInteger::getBitLength()` returns the number of bits in the minimal representation of the number
|
||||
|
||||
## [0.8.12](https://github.com/brick/math/releases/tag/0.8.12) - 2020-02-03
|
||||
|
||||
🛠️ **Maintenance release**
|
||||
|
||||
Classes are now annotated for better static analysis with [psalm](https://psalm.dev/).
|
||||
|
||||
This is a maintenance release: no bug fixes, no new features, no breaking changes.
|
||||
|
||||
## [0.8.11](https://github.com/brick/math/releases/tag/0.8.11) - 2020-01-23
|
||||
|
||||
✨ **New feature**
|
||||
|
||||
`BigInteger::powerMod()` performs a power-with-modulo operation. Useful for crypto.
|
||||
|
||||
## [0.8.10](https://github.com/brick/math/releases/tag/0.8.10) - 2020-01-21
|
||||
|
||||
✨ **New feature**
|
||||
|
||||
`BigInteger::mod()` returns the **modulo** of two numbers. The *modulo* differs from the *remainder* when the signs of the operands are different.
|
||||
|
||||
## [0.8.9](https://github.com/brick/math/releases/tag/0.8.9) - 2020-01-08
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
A few additional optimizations in `BigInteger` and `BigDecimal` when one of the operands can be returned as is. Thanks to @tomtomsen in #24.
|
||||
|
||||
## [0.8.8](https://github.com/brick/math/releases/tag/0.8.8) - 2019-04-25
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::toBase()` could return an empty string for zero values (BCMath & Native calculators only, GMP calculator unaffected)
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::toArbitraryBase()` converts a number to an arbitrary base, using a custom alphabet
|
||||
- `BigInteger::fromArbitraryBase()` converts a string in an arbitrary base, using a custom alphabet, back to a number
|
||||
|
||||
These methods can be used as the foundation to convert strings between different bases/alphabets, using BigInteger as an intermediate representation.
|
||||
|
||||
💩 **Deprecations**
|
||||
|
||||
- `BigInteger::parse()` is now deprecated in favour of `fromBase()`
|
||||
|
||||
`BigInteger::fromBase()` works the same way as `parse()`, with 2 minor differences:
|
||||
|
||||
- the `$base` parameter is required, it does not default to `10`
|
||||
- it throws a `NumberFormatException` instead of an `InvalidArgumentException` when the number is malformed
|
||||
|
||||
## [0.8.7](https://github.com/brick/math/releases/tag/0.8.7) - 2019-04-20
|
||||
|
||||
**Improvements**
|
||||
|
||||
- Safer conversion from `float` when using custom locales
|
||||
- **Much faster** `NativeCalculator` implementation 🚀
|
||||
|
||||
You can expect **at least a 3x performance improvement** for common arithmetic operations when using the library on systems without GMP or BCMath; it gets exponentially faster on multiplications with a high number of digits. This is due to calculations now being performed on whole blocks of digits (the block size depending on the platform, 32-bit or 64-bit) instead of digit-by-digit as before.
|
||||
|
||||
## [0.8.6](https://github.com/brick/math/releases/tag/0.8.6) - 2019-04-11
|
||||
|
||||
**New method**
|
||||
|
||||
`BigNumber::sum()` returns the sum of one or more numbers.
|
||||
|
||||
## [0.8.5](https://github.com/brick/math/releases/tag/0.8.5) - 2019-02-12
|
||||
|
||||
**Bug fix**: `of()` factory methods could fail when passing a `float` in environments using a `LC_NUMERIC` locale with a decimal separator other than `'.'` (#20).
|
||||
|
||||
Thanks @manowark 👍
|
||||
|
||||
## [0.8.4](https://github.com/brick/math/releases/tag/0.8.4) - 2018-12-07
|
||||
|
||||
**New method**
|
||||
|
||||
`BigDecimal::sqrt()` calculates the square root of a decimal number, to a given scale.
|
||||
|
||||
## [0.8.3](https://github.com/brick/math/releases/tag/0.8.3) - 2018-12-06
|
||||
|
||||
**New method**
|
||||
|
||||
`BigInteger::sqrt()` calculates the square root of a number (thanks @peter279k).
|
||||
|
||||
**New exception**
|
||||
|
||||
`NegativeNumberException` is thrown when calling `sqrt()` on a negative number.
|
||||
|
||||
## [0.8.2](https://github.com/brick/math/releases/tag/0.8.2) - 2018-11-08
|
||||
|
||||
**Performance update**
|
||||
|
||||
- Further improvement of `toInt()` performance
|
||||
- `NativeCalculator` can now perform some multiplications more efficiently
|
||||
|
||||
## [0.8.1](https://github.com/brick/math/releases/tag/0.8.1) - 2018-11-07
|
||||
|
||||
Performance optimization of `toInt()` methods.
|
||||
|
||||
## [0.8.0](https://github.com/brick/math/releases/tag/0.8.0) - 2018-10-13
|
||||
|
||||
**Breaking changes**
|
||||
|
||||
The following deprecated methods have been removed. Use the new method name instead:
|
||||
|
||||
| Method removed | Replacement method |
|
||||
| --- | --- |
|
||||
| `BigDecimal::getIntegral()` | `BigDecimal::getIntegralPart()` |
|
||||
| `BigDecimal::getFraction()` | `BigDecimal::getFractionalPart()` |
|
||||
|
||||
---
|
||||
|
||||
**New features**
|
||||
|
||||
`BigInteger` has been augmented with 5 new methods for bitwise operations:
|
||||
|
||||
| New method | Description |
|
||||
| --- | --- |
|
||||
| `and()` | performs a bitwise `AND` operation on two numbers |
|
||||
| `or()` | performs a bitwise `OR` operation on two numbers |
|
||||
| `xor()` | performs a bitwise `XOR` operation on two numbers |
|
||||
| `shiftedLeft()` | returns the number shifted left by a number of bits |
|
||||
| `shiftedRight()` | returns the number shifted right by a number of bits |
|
||||
|
||||
Thanks to @DASPRiD 👍
|
||||
|
||||
## [0.7.3](https://github.com/brick/math/releases/tag/0.7.3) - 2018-08-20
|
||||
|
||||
**New method:** `BigDecimal::hasNonZeroFractionalPart()`
|
||||
|
||||
**Renamed/deprecated methods:**
|
||||
|
||||
- `BigDecimal::getIntegral()` has been renamed to `getIntegralPart()` and is now deprecated
|
||||
- `BigDecimal::getFraction()` has been renamed to `getFractionalPart()` and is now deprecated
|
||||
|
||||
## [0.7.2](https://github.com/brick/math/releases/tag/0.7.2) - 2018-07-21
|
||||
|
||||
**Performance update**
|
||||
|
||||
`BigInteger::parse()` and `toBase()` now use GMP's built-in base conversion features when available.
|
||||
|
||||
## [0.7.1](https://github.com/brick/math/releases/tag/0.7.1) - 2018-03-01
|
||||
|
||||
This is a maintenance release, no code has been changed.
|
||||
|
||||
- When installed with `--no-dev`, the autoloader does not autoload tests anymore
|
||||
- Tests and other files unnecessary for production are excluded from the dist package
|
||||
|
||||
This will help make installations more compact.
|
||||
|
||||
## [0.7.0](https://github.com/brick/math/releases/tag/0.7.0) - 2017-10-02
|
||||
|
||||
Methods renamed:
|
||||
|
||||
- `BigNumber:sign()` has been renamed to `getSign()`
|
||||
- `BigDecimal::unscaledValue()` has been renamed to `getUnscaledValue()`
|
||||
- `BigDecimal::scale()` has been renamed to `getScale()`
|
||||
- `BigDecimal::integral()` has been renamed to `getIntegral()`
|
||||
- `BigDecimal::fraction()` has been renamed to `getFraction()`
|
||||
- `BigRational::numerator()` has been renamed to `getNumerator()`
|
||||
- `BigRational::denominator()` has been renamed to `getDenominator()`
|
||||
|
||||
Classes renamed:
|
||||
|
||||
- `ArithmeticException` has been renamed to `MathException`
|
||||
|
||||
## [0.6.2](https://github.com/brick/math/releases/tag/0.6.2) - 2017-10-02
|
||||
|
||||
The base class for all exceptions is now `MathException`.
|
||||
`ArithmeticException` has been deprecated, and will be removed in 0.7.0.
|
||||
|
||||
## [0.6.1](https://github.com/brick/math/releases/tag/0.6.1) - 2017-10-02
|
||||
|
||||
A number of methods have been renamed:
|
||||
|
||||
- `BigNumber:sign()` is deprecated; use `getSign()` instead
|
||||
- `BigDecimal::unscaledValue()` is deprecated; use `getUnscaledValue()` instead
|
||||
- `BigDecimal::scale()` is deprecated; use `getScale()` instead
|
||||
- `BigDecimal::integral()` is deprecated; use `getIntegral()` instead
|
||||
- `BigDecimal::fraction()` is deprecated; use `getFraction()` instead
|
||||
- `BigRational::numerator()` is deprecated; use `getNumerator()` instead
|
||||
- `BigRational::denominator()` is deprecated; use `getDenominator()` instead
|
||||
|
||||
The old methods will be removed in version 0.7.0.
|
||||
|
||||
## [0.6.0](https://github.com/brick/math/releases/tag/0.6.0) - 2017-08-25
|
||||
|
||||
- Minimum PHP version is now [7.1](https://gophp71.org/); for PHP 5.6 and PHP 7.0 support, use version `0.5`
|
||||
- Deprecated method `BigDecimal::withScale()` has been removed; use `toScale()` instead
|
||||
- Method `BigNumber::toInteger()` has been renamed to `toInt()`
|
||||
|
||||
## [0.5.4](https://github.com/brick/math/releases/tag/0.5.4) - 2016-10-17
|
||||
|
||||
`BigNumber` classes now implement [JsonSerializable](http://php.net/manual/en/class.jsonserializable.php).
|
||||
The JSON output is always a string.
|
||||
|
||||
## [0.5.3](https://github.com/brick/math/releases/tag/0.5.3) - 2016-03-31
|
||||
|
||||
This is a bugfix release. Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||
|
||||
## [0.5.2](https://github.com/brick/math/releases/tag/0.5.2) - 2015-08-06
|
||||
|
||||
The `$scale` parameter of `BigDecimal::dividedBy()` is now optional again.
|
||||
|
||||
## [0.5.1](https://github.com/brick/math/releases/tag/0.5.1) - 2015-07-05
|
||||
|
||||
**New method: `BigNumber::toScale()`**
|
||||
|
||||
This allows to convert any `BigNumber` to a `BigDecimal` with a given scale, using rounding if necessary.
|
||||
|
||||
## [0.5.0](https://github.com/brick/math/releases/tag/0.5.0) - 2015-07-04
|
||||
|
||||
**New features**
|
||||
- Common `BigNumber` interface for all classes, with the following methods:
|
||||
- `sign()` and derived methods (`isZero()`, `isPositive()`, ...)
|
||||
- `compareTo()` and derived methods (`isEqualTo()`, `isGreaterThan()`, ...) that work across different `BigNumber` types
|
||||
- `toBigInteger()`, `toBigDecimal()`, `toBigRational`() conversion methods
|
||||
- `toInteger()` and `toFloat()` conversion methods to native types
|
||||
- Unified `of()` behaviour: every class now accepts any type of number, provided that it can be safely converted to the current type
|
||||
- New method: `BigDecimal::exactlyDividedBy()`; this method automatically computes the scale of the result, provided that the division yields a finite number of digits
|
||||
- New methods: `BigRational::quotient()` and `remainder()`
|
||||
- Fine-grained exceptions: `DivisionByZeroException`, `RoundingNecessaryException`, `NumberFormatException`
|
||||
- Factory methods `zero()`, `one()` and `ten()` available in all classes
|
||||
- Rounding mode reintroduced in `BigInteger::dividedBy()`
|
||||
|
||||
This release also comes with many performance improvements.
|
||||
|
||||
---
|
||||
|
||||
**Breaking changes**
|
||||
- `BigInteger`:
|
||||
- `getSign()` is renamed to `sign()`
|
||||
- `toString()` is renamed to `toBase()`
|
||||
- `BigInteger::dividedBy()` now throws an exception by default if the remainder is not zero; use `quotient()` to get the previous behaviour
|
||||
- `BigDecimal`:
|
||||
- `getSign()` is renamed to `sign()`
|
||||
- `getUnscaledValue()` is renamed to `unscaledValue()`
|
||||
- `getScale()` is renamed to `scale()`
|
||||
- `getIntegral()` is renamed to `integral()`
|
||||
- `getFraction()` is renamed to `fraction()`
|
||||
- `divideAndRemainder()` is renamed to `quotientAndRemainder()`
|
||||
- `dividedBy()` now takes a **mandatory** `$scale` parameter **before** the rounding mode
|
||||
- `toBigInteger()` does not accept a `$roundingMode` parameter any more
|
||||
- `toBigRational()` does not simplify the fraction any more; explicitly add `->simplified()` to get the previous behaviour
|
||||
- `BigRational`:
|
||||
- `getSign()` is renamed to `sign()`
|
||||
- `getNumerator()` is renamed to `numerator()`
|
||||
- `getDenominator()` is renamed to `denominator()`
|
||||
- `of()` is renamed to `nd()`, while `parse()` is renamed to `of()`
|
||||
- Miscellaneous:
|
||||
- `ArithmeticException` is moved to an `Exception\` sub-namespace
|
||||
- `of()` factory methods now throw `NumberFormatException` instead of `InvalidArgumentException`
|
||||
|
||||
## [0.4.3](https://github.com/brick/math/releases/tag/0.4.3) - 2016-03-31
|
||||
|
||||
Backport of two bug fixes from the 0.5 branch:
|
||||
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
|
||||
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||
|
||||
## [0.4.2](https://github.com/brick/math/releases/tag/0.4.2) - 2015-06-16
|
||||
|
||||
New method: `BigDecimal::stripTrailingZeros()`
|
||||
|
||||
## [0.4.1](https://github.com/brick/math/releases/tag/0.4.1) - 2015-06-12
|
||||
|
||||
Introducing a `BigRational` class, to perform calculations on fractions of any size.
|
||||
|
||||
## [0.4.0](https://github.com/brick/math/releases/tag/0.4.0) - 2015-06-12
|
||||
|
||||
Rounding modes have been removed from `BigInteger`, and are now a concept specific to `BigDecimal`.
|
||||
|
||||
`BigInteger::dividedBy()` now always returns the quotient of the division.
|
||||
|
||||
## [0.3.5](https://github.com/brick/math/releases/tag/0.3.5) - 2016-03-31
|
||||
|
||||
Backport of two bug fixes from the 0.5 branch:
|
||||
|
||||
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
|
||||
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||
|
||||
## [0.3.4](https://github.com/brick/math/releases/tag/0.3.4) - 2015-06-11
|
||||
|
||||
New methods:
|
||||
- `BigInteger::remainder()` returns the remainder of a division only
|
||||
- `BigInteger::gcd()` returns the greatest common divisor of two numbers
|
||||
|
||||
## [0.3.3](https://github.com/brick/math/releases/tag/0.3.3) - 2015-06-07
|
||||
|
||||
Fix `toString()` not handling negative numbers.
|
||||
|
||||
## [0.3.2](https://github.com/brick/math/releases/tag/0.3.2) - 2015-06-07
|
||||
|
||||
`BigInteger` and `BigDecimal` now have a `getSign()` method that returns:
|
||||
- `-1` if the number is negative
|
||||
- `0` if the number is zero
|
||||
- `1` if the number is positive
|
||||
|
||||
## [0.3.1](https://github.com/brick/math/releases/tag/0.3.1) - 2015-06-05
|
||||
|
||||
Minor performance improvements
|
||||
|
||||
## [0.3.0](https://github.com/brick/math/releases/tag/0.3.0) - 2015-06-04
|
||||
|
||||
The `$roundingMode` and `$scale` parameters have been swapped in `BigDecimal::dividedBy()`.
|
||||
|
||||
## [0.2.2](https://github.com/brick/math/releases/tag/0.2.2) - 2015-06-04
|
||||
|
||||
Stronger immutability guarantee for `BigInteger` and `BigDecimal`.
|
||||
|
||||
So far, it would have been possible to break immutability of these classes by calling the `unserialize()` internal function. This release fixes that.
|
||||
|
||||
## [0.2.1](https://github.com/brick/math/releases/tag/0.2.1) - 2015-06-02
|
||||
|
||||
Added `BigDecimal::divideAndRemainder()`
|
||||
|
||||
## [0.2.0](https://github.com/brick/math/releases/tag/0.2.0) - 2015-05-22
|
||||
|
||||
- `min()` and `max()` do not accept an `array` any more, but a variable number of parameters
|
||||
- **minimum PHP version is now 5.6**
|
||||
- continuous integration with PHP 7
|
||||
|
||||
## [0.1.1](https://github.com/brick/math/releases/tag/0.1.1) - 2014-09-01
|
||||
|
||||
- Added `BigInteger::power()`
|
||||
- Added HHVM support
|
||||
|
||||
## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31
|
||||
|
||||
First beta release.
|
||||
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-present Benjamin Morel
|
||||
|
||||
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, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+861
@@ -0,0 +1,861 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NegativeNumberException;
|
||||
use Brick\Math\Internal\Calculator;
|
||||
|
||||
/**
|
||||
* Immutable, arbitrary-precision signed decimal numbers.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class BigDecimal extends BigNumber
|
||||
{
|
||||
/**
|
||||
* The unscaled value of this decimal number.
|
||||
*
|
||||
* This is a string of digits with an optional leading minus sign.
|
||||
* No leading zero must be present.
|
||||
* No leading minus sign must be present if the value is 0.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* The scale (number of digits after the decimal point) of this decimal number.
|
||||
*
|
||||
* This must be zero or more.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $scale;
|
||||
|
||||
/**
|
||||
* Protected constructor. Use a factory method to obtain an instance.
|
||||
*
|
||||
* @param string $value The unscaled value, validated.
|
||||
* @param int $scale The scale, validated.
|
||||
*/
|
||||
protected function __construct(string $value, int $scale = 0)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->scale = $scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigDecimal of the given value.
|
||||
*
|
||||
* @param BigNumber|int|float|string $value
|
||||
*
|
||||
* @return BigDecimal
|
||||
*
|
||||
* @throws MathException If the value cannot be converted to a BigDecimal.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function of($value) : BigNumber
|
||||
{
|
||||
return parent::of($value)->toBigDecimal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigDecimal from an unscaled value and a scale.
|
||||
*
|
||||
* Example: `(12345, 3)` will result in the BigDecimal `12.345`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
|
||||
* @param int $scale The scale of the number, positive or zero.
|
||||
*
|
||||
* @return BigDecimal
|
||||
*
|
||||
* @throws \InvalidArgumentException If the scale is negative.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function ofUnscaledValue($value, int $scale = 0) : BigDecimal
|
||||
{
|
||||
if ($scale < 0) {
|
||||
throw new \InvalidArgumentException('The scale cannot be negative.');
|
||||
}
|
||||
|
||||
return new BigDecimal((string) BigInteger::of($value), $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigDecimal representing zero, with a scale of zero.
|
||||
*
|
||||
* @return BigDecimal
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function zero() : BigDecimal
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigDecimal|null $zero
|
||||
*/
|
||||
static $zero;
|
||||
|
||||
if ($zero === null) {
|
||||
$zero = new BigDecimal('0');
|
||||
}
|
||||
|
||||
return $zero;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigDecimal representing one, with a scale of zero.
|
||||
*
|
||||
* @return BigDecimal
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function one() : BigDecimal
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigDecimal|null $one
|
||||
*/
|
||||
static $one;
|
||||
|
||||
if ($one === null) {
|
||||
$one = new BigDecimal('1');
|
||||
}
|
||||
|
||||
return $one;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigDecimal representing ten, with a scale of zero.
|
||||
*
|
||||
* @return BigDecimal
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function ten() : BigDecimal
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigDecimal|null $ten
|
||||
*/
|
||||
static $ten;
|
||||
|
||||
if ($ten === null) {
|
||||
$ten = new BigDecimal('10');
|
||||
}
|
||||
|
||||
return $ten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of this number and the given one.
|
||||
*
|
||||
* The result has a scale of `max($this->scale, $that->scale)`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @return BigDecimal The result.
|
||||
*
|
||||
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
|
||||
*/
|
||||
public function plus($that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '0' && $that->scale <= $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->value === '0' && $this->scale <= $that->scale) {
|
||||
return $that;
|
||||
}
|
||||
|
||||
[$a, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
$value = Calculator::get()->add($a, $b);
|
||||
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference of this number and the given one.
|
||||
*
|
||||
* The result has a scale of `max($this->scale, $that->scale)`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @return BigDecimal The result.
|
||||
*
|
||||
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
|
||||
*/
|
||||
public function minus($that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '0' && $that->scale <= $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
[$a, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
$value = Calculator::get()->sub($a, $b);
|
||||
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the product of this number and the given one.
|
||||
*
|
||||
* The result has a scale of `$this->scale + $that->scale`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @return BigDecimal The result.
|
||||
*
|
||||
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
|
||||
*/
|
||||
public function multipliedBy($that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '1' && $that->scale === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->value === '1' && $this->scale === 0) {
|
||||
return $that;
|
||||
}
|
||||
|
||||
$value = Calculator::get()->mul($this->value, $that->value);
|
||||
$scale = $this->scale + $that->scale;
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of the division of this number by the given one, at the given scale.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor.
|
||||
* @param int|null $scale The desired scale, or null to use the scale of this number.
|
||||
* @param int $roundingMode An optional rounding mode.
|
||||
*
|
||||
* @return BigDecimal
|
||||
*
|
||||
* @throws \InvalidArgumentException If the scale or rounding mode is invalid.
|
||||
* @throws MathException If the number is invalid, is zero, or rounding was necessary.
|
||||
*/
|
||||
public function dividedBy($that, ?int $scale = null, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
if ($scale === null) {
|
||||
$scale = $this->scale;
|
||||
} elseif ($scale < 0) {
|
||||
throw new \InvalidArgumentException('Scale cannot be negative.');
|
||||
}
|
||||
|
||||
if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale + $scale);
|
||||
$q = $that->valueWithMinScale($this->scale - $scale);
|
||||
|
||||
$result = Calculator::get()->divRound($p, $q, $roundingMode);
|
||||
|
||||
return new BigDecimal($result, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the exact result of the division of this number by the given one.
|
||||
*
|
||||
* The scale of the result is automatically calculated to fit all the fraction digits.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @return BigDecimal The result.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
|
||||
* or the result yields an infinite number of digits.
|
||||
*/
|
||||
public function exactlyDividedBy($that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->value === '0') {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
[, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
$d = \rtrim($b, '0');
|
||||
$scale = \strlen($b) - \strlen($d);
|
||||
|
||||
$calculator = Calculator::get();
|
||||
|
||||
foreach ([5, 2] as $prime) {
|
||||
for (;;) {
|
||||
$lastDigit = (int) $d[-1];
|
||||
|
||||
if ($lastDigit % $prime !== 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$d = $calculator->divQ($d, (string) $prime);
|
||||
$scale++;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->dividedBy($that, $scale)->stripTrailingZeros();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this number exponentiated to the given value.
|
||||
*
|
||||
* The result has a scale of `$this->scale * $exponent`.
|
||||
*
|
||||
* @param int $exponent The exponent.
|
||||
*
|
||||
* @return BigDecimal The result.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
|
||||
*/
|
||||
public function power(int $exponent) : BigDecimal
|
||||
{
|
||||
if ($exponent === 0) {
|
||||
return BigDecimal::one();
|
||||
}
|
||||
|
||||
if ($exponent === 1) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
|
||||
throw new \InvalidArgumentException(\sprintf(
|
||||
'The exponent %d is not in the range 0 to %d.',
|
||||
$exponent,
|
||||
Calculator::MAX_POWER
|
||||
));
|
||||
}
|
||||
|
||||
return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of this number by this given one.
|
||||
*
|
||||
* The quotient has a scale of `0`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @return BigDecimal The quotient.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid decimal number, or is zero.
|
||||
*/
|
||||
public function quotient($that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale);
|
||||
$q = $that->valueWithMinScale($this->scale);
|
||||
|
||||
$quotient = Calculator::get()->divQ($p, $q);
|
||||
|
||||
return new BigDecimal($quotient, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of this number by this given one.
|
||||
*
|
||||
* The remainder has a scale of `max($this->scale, $that->scale)`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @return BigDecimal The remainder.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid decimal number, or is zero.
|
||||
*/
|
||||
public function remainder($that) : BigDecimal
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale);
|
||||
$q = $that->valueWithMinScale($this->scale);
|
||||
|
||||
$remainder = Calculator::get()->divR($p, $q);
|
||||
|
||||
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||
|
||||
return new BigDecimal($remainder, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient and remainder of the division of this number by the given one.
|
||||
*
|
||||
* The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
|
||||
*
|
||||
* @return BigDecimal[] An array containing the quotient and the remainder.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid decimal number, or is zero.
|
||||
*/
|
||||
public function quotientAndRemainder($that) : array
|
||||
{
|
||||
$that = BigDecimal::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$p = $this->valueWithMinScale($that->scale);
|
||||
$q = $that->valueWithMinScale($this->scale);
|
||||
|
||||
[$quotient, $remainder] = Calculator::get()->divQR($p, $q);
|
||||
|
||||
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
|
||||
|
||||
$quotient = new BigDecimal($quotient, 0);
|
||||
$remainder = new BigDecimal($remainder, $scale);
|
||||
|
||||
return [$quotient, $remainder];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the square root of this number, rounded down to the given number of decimals.
|
||||
*
|
||||
* @param int $scale
|
||||
*
|
||||
* @return BigDecimal
|
||||
*
|
||||
* @throws \InvalidArgumentException If the scale is negative.
|
||||
* @throws NegativeNumberException If this number is negative.
|
||||
*/
|
||||
public function sqrt(int $scale) : BigDecimal
|
||||
{
|
||||
if ($scale < 0) {
|
||||
throw new \InvalidArgumentException('Scale cannot be negative.');
|
||||
}
|
||||
|
||||
if ($this->value === '0') {
|
||||
return new BigDecimal('0', $scale);
|
||||
}
|
||||
|
||||
if ($this->value[0] === '-') {
|
||||
throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
|
||||
}
|
||||
|
||||
$value = $this->value;
|
||||
$addDigits = 2 * $scale - $this->scale;
|
||||
|
||||
if ($addDigits > 0) {
|
||||
// add zeros
|
||||
$value .= \str_repeat('0', $addDigits);
|
||||
} elseif ($addDigits < 0) {
|
||||
// trim digits
|
||||
if (-$addDigits >= \strlen($this->value)) {
|
||||
// requesting a scale too low, will always yield a zero result
|
||||
return new BigDecimal('0', $scale);
|
||||
}
|
||||
|
||||
$value = \substr($value, 0, $addDigits);
|
||||
}
|
||||
|
||||
$value = Calculator::get()->sqrt($value);
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this BigDecimal with the decimal point moved $n places to the left.
|
||||
*
|
||||
* @param int $n
|
||||
*
|
||||
* @return BigDecimal
|
||||
*/
|
||||
public function withPointMovedLeft(int $n) : BigDecimal
|
||||
{
|
||||
if ($n === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($n < 0) {
|
||||
return $this->withPointMovedRight(-$n);
|
||||
}
|
||||
|
||||
return new BigDecimal($this->value, $this->scale + $n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this BigDecimal with the decimal point moved $n places to the right.
|
||||
*
|
||||
* @param int $n
|
||||
*
|
||||
* @return BigDecimal
|
||||
*/
|
||||
public function withPointMovedRight(int $n) : BigDecimal
|
||||
{
|
||||
if ($n === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($n < 0) {
|
||||
return $this->withPointMovedLeft(-$n);
|
||||
}
|
||||
|
||||
$value = $this->value;
|
||||
$scale = $this->scale - $n;
|
||||
|
||||
if ($scale < 0) {
|
||||
if ($value !== '0') {
|
||||
$value .= \str_repeat('0', -$scale);
|
||||
}
|
||||
$scale = 0;
|
||||
}
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
|
||||
*
|
||||
* @return BigDecimal
|
||||
*/
|
||||
public function stripTrailingZeros() : BigDecimal
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$trimmedValue = \rtrim($this->value, '0');
|
||||
|
||||
if ($trimmedValue === '') {
|
||||
return BigDecimal::zero();
|
||||
}
|
||||
|
||||
$trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
|
||||
|
||||
if ($trimmableZeros === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($trimmableZeros > $this->scale) {
|
||||
$trimmableZeros = $this->scale;
|
||||
}
|
||||
|
||||
$value = \substr($this->value, 0, -$trimmableZeros);
|
||||
$scale = $this->scale - $trimmableZeros;
|
||||
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of this number.
|
||||
*
|
||||
* @return BigDecimal
|
||||
*/
|
||||
public function abs() : BigDecimal
|
||||
{
|
||||
return $this->isNegative() ? $this->negated() : $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the negated value of this number.
|
||||
*
|
||||
* @return BigDecimal
|
||||
*/
|
||||
public function negated() : BigDecimal
|
||||
{
|
||||
return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function compareTo($that) : int
|
||||
{
|
||||
$that = BigNumber::of($that);
|
||||
|
||||
if ($that instanceof BigInteger) {
|
||||
$that = $that->toBigDecimal();
|
||||
}
|
||||
|
||||
if ($that instanceof BigDecimal) {
|
||||
[$a, $b] = $this->scaleValues($this, $that);
|
||||
|
||||
return Calculator::get()->cmp($a, $b);
|
||||
}
|
||||
|
||||
return - $that->compareTo($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSign() : int
|
||||
{
|
||||
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BigInteger
|
||||
*/
|
||||
public function getUnscaledValue() : BigInteger
|
||||
{
|
||||
return BigInteger::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getScale() : int
|
||||
{
|
||||
return $this->scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the integral part of this decimal number.
|
||||
*
|
||||
* Example: `-123.456` => `-123`.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIntegralPart() : string
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
return \substr($value, 0, -$this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the fractional part of this decimal number.
|
||||
*
|
||||
* If the scale is zero, an empty string is returned.
|
||||
*
|
||||
* Examples: `-123.456` => '456', `123` => ''.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFractionalPart() : string
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
return \substr($value, -$this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this decimal number has a non-zero fractional part.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNonZeroFractionalPart() : bool
|
||||
{
|
||||
return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toBigInteger() : BigInteger
|
||||
{
|
||||
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
|
||||
|
||||
return BigInteger::create($zeroScaleDecimal->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toBigDecimal() : BigDecimal
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toBigRational() : BigRational
|
||||
{
|
||||
$numerator = BigInteger::create($this->value);
|
||||
$denominator = BigInteger::create('1' . \str_repeat('0', $this->scale));
|
||||
|
||||
return BigRational::create($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
|
||||
{
|
||||
if ($scale === $this->scale) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toInt() : int
|
||||
{
|
||||
return $this->toBigInteger()->toInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toFloat() : float
|
||||
{
|
||||
return (float) (string) $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() : string
|
||||
{
|
||||
if ($this->scale === 0) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
$value = $this->getUnscaledValueWithLeadingZeros();
|
||||
|
||||
return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function serialize() : string
|
||||
{
|
||||
return $this->value . ':' . $this->scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is only here to implement interface Serializable and cannot be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
* @psalm-suppress RedundantPropertyInitializationCheck
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function unserialize($value) : void
|
||||
{
|
||||
if (isset($this->value)) {
|
||||
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
|
||||
}
|
||||
|
||||
[$value, $scale] = \explode(':', $value);
|
||||
|
||||
$this->value = $value;
|
||||
$this->scale = (int) $scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the internal values of the given decimal numbers on the same scale.
|
||||
*
|
||||
* @param BigDecimal $x The first decimal number.
|
||||
* @param BigDecimal $y The second decimal number.
|
||||
*
|
||||
* @return array{0: string, 1: string} The scaled integer values of $x and $y.
|
||||
*/
|
||||
private function scaleValues(BigDecimal $x, BigDecimal $y) : array
|
||||
{
|
||||
$a = $x->value;
|
||||
$b = $y->value;
|
||||
|
||||
if ($b !== '0' && $x->scale > $y->scale) {
|
||||
$b .= \str_repeat('0', $x->scale - $y->scale);
|
||||
} elseif ($a !== '0' && $x->scale < $y->scale) {
|
||||
$a .= \str_repeat('0', $y->scale - $x->scale);
|
||||
}
|
||||
|
||||
return [$a, $b];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $scale
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function valueWithMinScale(int $scale) : string
|
||||
{
|
||||
$value = $this->value;
|
||||
|
||||
if ($this->value !== '0' && $scale > $this->scale) {
|
||||
$value .= \str_repeat('0', $scale - $this->scale);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getUnscaledValueWithLeadingZeros() : string
|
||||
{
|
||||
$value = $this->value;
|
||||
$targetLength = $this->scale + 1;
|
||||
$negative = ($value[0] === '-');
|
||||
$length = \strlen($value);
|
||||
|
||||
if ($negative) {
|
||||
$length--;
|
||||
}
|
||||
|
||||
if ($length >= $targetLength) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
if ($negative) {
|
||||
$value = \substr($value, 1);
|
||||
}
|
||||
|
||||
$value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT);
|
||||
|
||||
if ($negative) {
|
||||
$value = '-' . $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
+1151
File diff suppressed because it is too large.
Load diff
+572
@@ -0,0 +1,572 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NumberFormatException;
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
|
||||
/**
|
||||
* Common interface for arbitrary-precision rational numbers.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
abstract class BigNumber implements \Serializable, \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* The regular expression used to parse integer, decimal and rational numbers.
|
||||
*/
|
||||
private const PARSE_REGEXP =
|
||||
'/^' .
|
||||
'(?<sign>[\-\+])?' .
|
||||
'(?:' .
|
||||
'(?:' .
|
||||
'(?<integral>[0-9]+)?' .
|
||||
'(?<point>\.)?' .
|
||||
'(?<fractional>[0-9]+)?' .
|
||||
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
|
||||
')|(?:' .
|
||||
'(?<numerator>[0-9]+)' .
|
||||
'\/?' .
|
||||
'(?<denominator>[0-9]+)' .
|
||||
')' .
|
||||
')' .
|
||||
'$/';
|
||||
|
||||
/**
|
||||
* Creates a BigNumber of the given value.
|
||||
*
|
||||
* The concrete return type is dependent on the given value, with the following rules:
|
||||
*
|
||||
* - BigNumber instances are returned as is
|
||||
* - integer numbers are returned as BigInteger
|
||||
* - floating point numbers are converted to a string then parsed as such
|
||||
* - strings containing a `/` character are returned as BigRational
|
||||
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
|
||||
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
|
||||
*
|
||||
* @param BigNumber|int|float|string $value
|
||||
*
|
||||
* @return BigNumber
|
||||
*
|
||||
* @throws NumberFormatException If the format of the number is not valid.
|
||||
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function of($value) : BigNumber
|
||||
{
|
||||
if ($value instanceof BigNumber) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (\is_int($value)) {
|
||||
return new BigInteger((string) $value);
|
||||
}
|
||||
|
||||
/** @psalm-suppress RedundantCastGivenDocblockType We cannot trust the untyped $value here! */
|
||||
$value = \is_float($value) ? self::floatToString($value) : (string) $value;
|
||||
|
||||
$throw = static function() use ($value) : void {
|
||||
throw new NumberFormatException(\sprintf(
|
||||
'The given value "%s" does not represent a valid number.',
|
||||
$value
|
||||
));
|
||||
};
|
||||
|
||||
if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) {
|
||||
$throw();
|
||||
}
|
||||
|
||||
$getMatch = static function(string $value) use ($matches) : ?string {
|
||||
return isset($matches[$value]) && $matches[$value] !== '' ? $matches[$value] : null;
|
||||
};
|
||||
|
||||
$sign = $getMatch('sign');
|
||||
$numerator = $getMatch('numerator');
|
||||
$denominator = $getMatch('denominator');
|
||||
|
||||
if ($numerator !== null) {
|
||||
assert($denominator !== null);
|
||||
|
||||
if ($sign !== null) {
|
||||
$numerator = $sign . $numerator;
|
||||
}
|
||||
|
||||
$numerator = self::cleanUp($numerator);
|
||||
$denominator = self::cleanUp($denominator);
|
||||
|
||||
if ($denominator === '0') {
|
||||
throw DivisionByZeroException::denominatorMustNotBeZero();
|
||||
}
|
||||
|
||||
return new BigRational(
|
||||
new BigInteger($numerator),
|
||||
new BigInteger($denominator),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
$point = $getMatch('point');
|
||||
$integral = $getMatch('integral');
|
||||
$fractional = $getMatch('fractional');
|
||||
$exponent = $getMatch('exponent');
|
||||
|
||||
if ($integral === null && $fractional === null) {
|
||||
$throw();
|
||||
}
|
||||
|
||||
if ($integral === null) {
|
||||
$integral = '0';
|
||||
}
|
||||
|
||||
if ($point !== null || $exponent !== null) {
|
||||
$fractional = ($fractional ?? '');
|
||||
$exponent = ($exponent !== null) ? (int) $exponent : 0;
|
||||
|
||||
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
|
||||
throw new NumberFormatException('Exponent too large.');
|
||||
}
|
||||
|
||||
$unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional);
|
||||
|
||||
$scale = \strlen($fractional) - $exponent;
|
||||
|
||||
if ($scale < 0) {
|
||||
if ($unscaledValue !== '0') {
|
||||
$unscaledValue .= \str_repeat('0', - $scale);
|
||||
}
|
||||
$scale = 0;
|
||||
}
|
||||
|
||||
return new BigDecimal($unscaledValue, $scale);
|
||||
}
|
||||
|
||||
$integral = self::cleanUp(($sign ?? '') . $integral);
|
||||
|
||||
return new BigInteger($integral);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely converts float to string, avoiding locale-dependent issues.
|
||||
*
|
||||
* @see https://github.com/brick/math/pull/20
|
||||
*
|
||||
* @param float $float
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @psalm-pure
|
||||
* @psalm-suppress ImpureFunctionCall
|
||||
*/
|
||||
private static function floatToString(float $float) : string
|
||||
{
|
||||
$currentLocale = \setlocale(LC_NUMERIC, '0');
|
||||
\setlocale(LC_NUMERIC, 'C');
|
||||
|
||||
$result = (string) $float;
|
||||
|
||||
\setlocale(LC_NUMERIC, $currentLocale);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy method to access protected constructors from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param mixed ...$args The arguments to the constructor.
|
||||
*
|
||||
* @return static
|
||||
*
|
||||
* @psalm-pure
|
||||
* @psalm-suppress TooManyArguments
|
||||
* @psalm-suppress UnsafeInstantiation
|
||||
*/
|
||||
protected static function create(... $args) : BigNumber
|
||||
{
|
||||
return new static(... $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum of the given values.
|
||||
*
|
||||
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
|
||||
* to an instance of the class this method is called on.
|
||||
*
|
||||
* @return static The minimum value.
|
||||
*
|
||||
* @throws \InvalidArgumentException If no values are given.
|
||||
* @throws MathException If an argument is not valid.
|
||||
*
|
||||
* @psalm-suppress LessSpecificReturnStatement
|
||||
* @psalm-suppress MoreSpecificReturnType
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function min(...$values) : BigNumber
|
||||
{
|
||||
$min = null;
|
||||
|
||||
foreach ($values as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
if ($min === null || $value->isLessThan($min)) {
|
||||
$min = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($min === null) {
|
||||
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||
}
|
||||
|
||||
return $min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum of the given values.
|
||||
*
|
||||
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
|
||||
* to an instance of the class this method is called on.
|
||||
*
|
||||
* @return static The maximum value.
|
||||
*
|
||||
* @throws \InvalidArgumentException If no values are given.
|
||||
* @throws MathException If an argument is not valid.
|
||||
*
|
||||
* @psalm-suppress LessSpecificReturnStatement
|
||||
* @psalm-suppress MoreSpecificReturnType
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function max(...$values) : BigNumber
|
||||
{
|
||||
$max = null;
|
||||
|
||||
foreach ($values as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
if ($max === null || $value->isGreaterThan($max)) {
|
||||
$max = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($max === null) {
|
||||
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of the given values.
|
||||
*
|
||||
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
|
||||
* to an instance of the class this method is called on.
|
||||
*
|
||||
* @return static The sum.
|
||||
*
|
||||
* @throws \InvalidArgumentException If no values are given.
|
||||
* @throws MathException If an argument is not valid.
|
||||
*
|
||||
* @psalm-suppress LessSpecificReturnStatement
|
||||
* @psalm-suppress MoreSpecificReturnType
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function sum(...$values) : BigNumber
|
||||
{
|
||||
/** @var BigNumber|null $sum */
|
||||
$sum = null;
|
||||
|
||||
foreach ($values as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
$sum = $sum === null ? $value : self::add($sum, $value);
|
||||
}
|
||||
|
||||
if ($sum === null) {
|
||||
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
|
||||
*
|
||||
* @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to
|
||||
* concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
|
||||
* depending on their ability to perform the operation. This will also require a version bump because we're
|
||||
* potentially breaking custom BigNumber implementations (if any...)
|
||||
*
|
||||
* @param BigNumber $a
|
||||
* @param BigNumber $b
|
||||
*
|
||||
* @return BigNumber
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
private static function add(BigNumber $a, BigNumber $b) : BigNumber
|
||||
{
|
||||
if ($a instanceof BigRational) {
|
||||
return $a->plus($b);
|
||||
}
|
||||
|
||||
if ($b instanceof BigRational) {
|
||||
return $b->plus($a);
|
||||
}
|
||||
|
||||
if ($a instanceof BigDecimal) {
|
||||
return $a->plus($b);
|
||||
}
|
||||
|
||||
if ($b instanceof BigDecimal) {
|
||||
return $b->plus($a);
|
||||
}
|
||||
|
||||
/** @var BigInteger $a */
|
||||
|
||||
return $a->plus($b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes optional leading zeros and + sign from the given number.
|
||||
*
|
||||
* @param string $number The number, validated as a non-empty string of digits with optional leading sign.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
private static function cleanUp(string $number) : string
|
||||
{
|
||||
$firstChar = $number[0];
|
||||
|
||||
if ($firstChar === '+' || $firstChar === '-') {
|
||||
$number = \substr($number, 1);
|
||||
}
|
||||
|
||||
$number = \ltrim($number, '0');
|
||||
|
||||
if ($number === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($firstChar === '-') {
|
||||
return '-' . $number;
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is equal to the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEqualTo($that) : bool
|
||||
{
|
||||
return $this->compareTo($that) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly lower than the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLessThan($that) : bool
|
||||
{
|
||||
return $this->compareTo($that) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is lower than or equal to the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLessThanOrEqualTo($that) : bool
|
||||
{
|
||||
return $this->compareTo($that) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly greater than the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isGreaterThan($that) : bool
|
||||
{
|
||||
return $this->compareTo($that) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is greater than or equal to the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isGreaterThanOrEqualTo($that) : bool
|
||||
{
|
||||
return $this->compareTo($that) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number equals zero.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isZero() : bool
|
||||
{
|
||||
return $this->getSign() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly negative.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNegative() : bool
|
||||
{
|
||||
return $this->getSign() < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is negative or zero.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNegativeOrZero() : bool
|
||||
{
|
||||
return $this->getSign() <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly positive.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPositive() : bool
|
||||
{
|
||||
return $this->getSign() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is positive or zero.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPositiveOrZero() : bool
|
||||
{
|
||||
return $this->getSign() >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sign of this number.
|
||||
*
|
||||
* @return int -1 if the number is negative, 0 if zero, 1 if positive.
|
||||
*/
|
||||
abstract public function getSign() : int;
|
||||
|
||||
/**
|
||||
* Compares this number to the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that
|
||||
*
|
||||
* @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*/
|
||||
abstract public function compareTo($that) : int;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigInteger.
|
||||
*
|
||||
* @return BigInteger The converted number.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
|
||||
*/
|
||||
abstract public function toBigInteger() : BigInteger;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigDecimal.
|
||||
*
|
||||
* @return BigDecimal The converted number.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
|
||||
*/
|
||||
abstract public function toBigDecimal() : BigDecimal;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigRational.
|
||||
*
|
||||
* @return BigRational The converted number.
|
||||
*/
|
||||
abstract public function toBigRational() : BigRational;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
|
||||
*
|
||||
* @param int $scale The scale of the resulting `BigDecimal`.
|
||||
* @param int $roundingMode A `RoundingMode` constant.
|
||||
*
|
||||
* @return BigDecimal
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
|
||||
* This only applies when RoundingMode::UNNECESSARY is used.
|
||||
*/
|
||||
abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
|
||||
|
||||
/**
|
||||
* Returns the exact value of this number as a native integer.
|
||||
*
|
||||
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
|
||||
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
|
||||
*
|
||||
* @return int The converted value.
|
||||
*
|
||||
* @throws MathException If this number cannot be exactly converted to a native integer.
|
||||
*/
|
||||
abstract public function toInt() : int;
|
||||
|
||||
/**
|
||||
* Returns an approximation of this number as a floating-point value.
|
||||
*
|
||||
* Note that this method can discard information as the precision of a floating-point value
|
||||
* is inherently limited.
|
||||
*
|
||||
* If the number is greater than the largest representable floating point number, positive infinity is returned.
|
||||
* If the number is less than the smallest representable floating point number, negative infinity is returned.
|
||||
*
|
||||
* @return float The converted value.
|
||||
*/
|
||||
abstract public function toFloat() : float;
|
||||
|
||||
/**
|
||||
* Returns a string representation of this number.
|
||||
*
|
||||
* The output of this method can be parsed by the `of()` factory method;
|
||||
* this will yield an object equal to this one, without any information loss.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function __toString() : string;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function jsonSerialize() : string
|
||||
{
|
||||
return $this->__toString();
|
||||
}
|
||||
}
|
||||
+489
@@ -0,0 +1,489 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NumberFormatException;
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
|
||||
/**
|
||||
* An arbitrarily large rational number.
|
||||
*
|
||||
* This class is immutable.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class BigRational extends BigNumber
|
||||
{
|
||||
/**
|
||||
* The numerator.
|
||||
*
|
||||
* @var BigInteger
|
||||
*/
|
||||
private $numerator;
|
||||
|
||||
/**
|
||||
* The denominator. Always strictly positive.
|
||||
*
|
||||
* @var BigInteger
|
||||
*/
|
||||
private $denominator;
|
||||
|
||||
/**
|
||||
* Protected constructor. Use a factory method to obtain an instance.
|
||||
*
|
||||
* @param BigInteger $numerator The numerator.
|
||||
* @param BigInteger $denominator The denominator.
|
||||
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
|
||||
*
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*/
|
||||
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
|
||||
{
|
||||
if ($checkDenominator) {
|
||||
if ($denominator->isZero()) {
|
||||
throw DivisionByZeroException::denominatorMustNotBeZero();
|
||||
}
|
||||
|
||||
if ($denominator->isNegative()) {
|
||||
$numerator = $numerator->negated();
|
||||
$denominator = $denominator->negated();
|
||||
}
|
||||
}
|
||||
|
||||
$this->numerator = $numerator;
|
||||
$this->denominator = $denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigRational of the given value.
|
||||
*
|
||||
* @param BigNumber|int|float|string $value
|
||||
*
|
||||
* @return BigRational
|
||||
*
|
||||
* @throws MathException If the value cannot be converted to a BigRational.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function of($value) : BigNumber
|
||||
{
|
||||
return parent::of($value)->toBigRational();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigRational out of a numerator and a denominator.
|
||||
*
|
||||
* If the denominator is negative, the signs of both the numerator and the denominator
|
||||
* will be inverted to ensure that the denominator is always positive.
|
||||
*
|
||||
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
|
||||
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
|
||||
*
|
||||
* @return BigRational
|
||||
*
|
||||
* @throws NumberFormatException If an argument does not represent a valid number.
|
||||
* @throws RoundingNecessaryException If an argument represents a non-integer number.
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function nd($numerator, $denominator) : BigRational
|
||||
{
|
||||
$numerator = BigInteger::of($numerator);
|
||||
$denominator = BigInteger::of($denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing zero.
|
||||
*
|
||||
* @return BigRational
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function zero() : BigRational
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigRational|null $zero
|
||||
*/
|
||||
static $zero;
|
||||
|
||||
if ($zero === null) {
|
||||
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
|
||||
}
|
||||
|
||||
return $zero;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing one.
|
||||
*
|
||||
* @return BigRational
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function one() : BigRational
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigRational|null $one
|
||||
*/
|
||||
static $one;
|
||||
|
||||
if ($one === null) {
|
||||
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
|
||||
}
|
||||
|
||||
return $one;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing ten.
|
||||
*
|
||||
* @return BigRational
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function ten() : BigRational
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress ImpureStaticVariable
|
||||
* @var BigRational|null $ten
|
||||
*/
|
||||
static $ten;
|
||||
|
||||
if ($ten === null) {
|
||||
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
|
||||
}
|
||||
|
||||
return $ten;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BigInteger
|
||||
*/
|
||||
public function getNumerator() : BigInteger
|
||||
{
|
||||
return $this->numerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BigInteger
|
||||
*/
|
||||
public function getDenominator() : BigInteger
|
||||
{
|
||||
return $this->denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of the numerator by the denominator.
|
||||
*
|
||||
* @return BigInteger
|
||||
*/
|
||||
public function quotient() : BigInteger
|
||||
{
|
||||
return $this->numerator->quotient($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of the numerator by the denominator.
|
||||
*
|
||||
* @return BigInteger
|
||||
*/
|
||||
public function remainder() : BigInteger
|
||||
{
|
||||
return $this->numerator->remainder($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quotient and remainder of the division of the numerator by the denominator.
|
||||
*
|
||||
* @return BigInteger[]
|
||||
*/
|
||||
public function quotientAndRemainder() : array
|
||||
{
|
||||
return $this->numerator->quotientAndRemainder($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to add.
|
||||
*
|
||||
* @return BigRational The result.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*/
|
||||
public function plus($that) : BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The number to subtract.
|
||||
*
|
||||
* @return BigRational The result.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*/
|
||||
public function minus($that) : BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the product of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The multiplier.
|
||||
*
|
||||
* @return BigRational The result.
|
||||
*
|
||||
* @throws MathException If the multiplier is not a valid number.
|
||||
*/
|
||||
public function multipliedBy($that) : BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->numerator);
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of the division of this number by the given one.
|
||||
*
|
||||
* @param BigNumber|int|float|string $that The divisor.
|
||||
*
|
||||
* @return BigRational The result.
|
||||
*
|
||||
* @throws MathException If the divisor is not a valid number, or is zero.
|
||||
*/
|
||||
public function dividedBy($that) : BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$denominator = $this->denominator->multipliedBy($that->numerator);
|
||||
|
||||
return new BigRational($numerator, $denominator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this number exponentiated to the given value.
|
||||
*
|
||||
* @param int $exponent The exponent.
|
||||
*
|
||||
* @return BigRational The result.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
|
||||
*/
|
||||
public function power(int $exponent) : BigRational
|
||||
{
|
||||
if ($exponent === 0) {
|
||||
$one = BigInteger::one();
|
||||
|
||||
return new BigRational($one, $one, false);
|
||||
}
|
||||
|
||||
if ($exponent === 1) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return new BigRational(
|
||||
$this->numerator->power($exponent),
|
||||
$this->denominator->power($exponent),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reciprocal of this BigRational.
|
||||
*
|
||||
* The reciprocal has the numerator and denominator swapped.
|
||||
*
|
||||
* @return BigRational
|
||||
*
|
||||
* @throws DivisionByZeroException If the numerator is zero.
|
||||
*/
|
||||
public function reciprocal() : BigRational
|
||||
{
|
||||
return new BigRational($this->denominator, $this->numerator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of this BigRational.
|
||||
*
|
||||
* @return BigRational
|
||||
*/
|
||||
public function abs() : BigRational
|
||||
{
|
||||
return new BigRational($this->numerator->abs(), $this->denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the negated value of this BigRational.
|
||||
*
|
||||
* @return BigRational
|
||||
*/
|
||||
public function negated() : BigRational
|
||||
{
|
||||
return new BigRational($this->numerator->negated(), $this->denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the simplified value of this BigRational.
|
||||
*
|
||||
* @return BigRational
|
||||
*/
|
||||
public function simplified() : BigRational
|
||||
{
|
||||
$gcd = $this->numerator->gcd($this->denominator);
|
||||
|
||||
$numerator = $this->numerator->quotient($gcd);
|
||||
$denominator = $this->denominator->quotient($gcd);
|
||||
|
||||
return new BigRational($numerator, $denominator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function compareTo($that) : int
|
||||
{
|
||||
return $this->minus($that)->getSign();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSign() : int
|
||||
{
|
||||
return $this->numerator->getSign();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toBigInteger() : BigInteger
|
||||
{
|
||||
$simplified = $this->simplified();
|
||||
|
||||
if (! $simplified->denominator->isEqualTo(1)) {
|
||||
throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.');
|
||||
}
|
||||
|
||||
return $simplified->numerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toBigDecimal() : BigDecimal
|
||||
{
|
||||
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toBigRational() : BigRational
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
|
||||
{
|
||||
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toInt() : int
|
||||
{
|
||||
return $this->toBigInteger()->toInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toFloat() : float
|
||||
{
|
||||
return $this->numerator->toFloat() / $this->denominator->toFloat();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() : string
|
||||
{
|
||||
$numerator = (string) $this->numerator;
|
||||
$denominator = (string) $this->denominator;
|
||||
|
||||
if ($denominator === '1') {
|
||||
return $numerator;
|
||||
}
|
||||
|
||||
return $this->numerator . '/' . $this->denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function serialize() : string
|
||||
{
|
||||
return $this->numerator . '/' . $this->denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is only here to implement interface Serializable and cannot be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
* @psalm-suppress RedundantPropertyInitializationCheck
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function unserialize($value) : void
|
||||
{
|
||||
if (isset($this->numerator)) {
|
||||
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
|
||||
}
|
||||
|
||||
[$numerator, $denominator] = \explode('/', $value);
|
||||
|
||||
$this->numerator = BigInteger::of($numerator);
|
||||
$this->denominator = BigInteger::of($denominator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a division by zero occurs.
|
||||
*/
|
||||
class DivisionByZeroException extends MathException
|
||||
{
|
||||
/**
|
||||
* @return DivisionByZeroException
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function divisionByZero() : DivisionByZeroException
|
||||
{
|
||||
return new self('Division by zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DivisionByZeroException
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function modulusMustNotBeZero() : DivisionByZeroException
|
||||
{
|
||||
return new self('The modulus must not be zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DivisionByZeroException
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function denominatorMustNotBeZero() : DivisionByZeroException
|
||||
{
|
||||
return new self('The denominator of a rational number cannot be zero.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use Brick\Math\BigInteger;
|
||||
|
||||
/**
|
||||
* Exception thrown when an integer overflow occurs.
|
||||
*/
|
||||
class IntegerOverflowException extends MathException
|
||||
{
|
||||
/**
|
||||
* @param BigInteger $value
|
||||
*
|
||||
* @return IntegerOverflowException
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
|
||||
{
|
||||
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
|
||||
|
||||
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Base class for all math exceptions.
|
||||
*
|
||||
* This class is abstract to ensure that only fine-grained exceptions are thrown throughout the code.
|
||||
*/
|
||||
class MathException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
|
||||
*/
|
||||
class NegativeNumberException extends MathException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to create a number from a string with an invalid format.
|
||||
*/
|
||||
class NumberFormatException extends MathException
|
||||
{
|
||||
/**
|
||||
* @param string $char The failing character.
|
||||
*
|
||||
* @return NumberFormatException
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function charNotInAlphabet(string $char) : self
|
||||
{
|
||||
$ord = \ord($char);
|
||||
|
||||
if ($ord < 32 || $ord > 126) {
|
||||
$char = \strtoupper(\dechex($ord));
|
||||
|
||||
if ($ord < 10) {
|
||||
$char = '0' . $char;
|
||||
}
|
||||
} else {
|
||||
$char = '"' . $char . '"';
|
||||
}
|
||||
|
||||
return new self(sprintf('Char %s is not a valid character in the given alphabet.', $char));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a number cannot be represented at the requested scale without rounding.
|
||||
*/
|
||||
class RoundingNecessaryException extends MathException
|
||||
{
|
||||
/**
|
||||
* @return RoundingNecessaryException
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function roundingNecessary() : RoundingNecessaryException
|
||||
{
|
||||
return new self('Rounding is necessary to represent the result of the operation at this scale.');
|
||||
}
|
||||
}
|
||||
+759
@@ -0,0 +1,759 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Brick\Math\RoundingMode;
|
||||
|
||||
/**
|
||||
* Performs basic operations on arbitrary size integers.
|
||||
*
|
||||
* Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
|
||||
* without leading zero, and with an optional leading minus sign if the number is not zero.
|
||||
*
|
||||
* Any other parameter format will lead to undefined behaviour.
|
||||
* All methods must return strings respecting this format, unless specified otherwise.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
abstract class Calculator
|
||||
{
|
||||
/**
|
||||
* The maximum exponent value allowed for the pow() method.
|
||||
*/
|
||||
public const MAX_POWER = 1000000;
|
||||
|
||||
/**
|
||||
* The alphabet for converting from and to base 2 to 36, lowercase.
|
||||
*/
|
||||
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
/**
|
||||
* The Calculator instance in use.
|
||||
*
|
||||
* @var Calculator|null
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Sets the Calculator instance to use.
|
||||
*
|
||||
* An instance is typically set only in unit tests: the autodetect is usually the best option.
|
||||
*
|
||||
* @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final public static function set(?Calculator $calculator) : void
|
||||
{
|
||||
self::$instance = $calculator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Calculator instance to use.
|
||||
*
|
||||
* If none has been explicitly set, the fastest available implementation will be returned.
|
||||
*
|
||||
* @return Calculator
|
||||
*
|
||||
* @psalm-pure
|
||||
* @psalm-suppress ImpureStaticProperty
|
||||
*/
|
||||
final public static function get() : Calculator
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
/** @psalm-suppress ImpureMethodCall */
|
||||
self::$instance = self::detect();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fastest available Calculator implementation.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return Calculator
|
||||
*/
|
||||
private static function detect() : Calculator
|
||||
{
|
||||
if (\extension_loaded('gmp')) {
|
||||
return new Calculator\GmpCalculator();
|
||||
}
|
||||
|
||||
if (\extension_loaded('bcmath')) {
|
||||
return new Calculator\BcMathCalculator();
|
||||
}
|
||||
|
||||
return new Calculator\NativeCalculator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the sign & digits of the operands.
|
||||
*
|
||||
* @param string $a The first operand.
|
||||
* @param string $b The second operand.
|
||||
*
|
||||
* @return array{0: bool, 1: bool, 2: string, 3: string} Whether $a and $b are negative, followed by their digits.
|
||||
*/
|
||||
final protected function init(string $a, string $b) : array
|
||||
{
|
||||
return [
|
||||
$aNeg = ($a[0] === '-'),
|
||||
$bNeg = ($b[0] === '-'),
|
||||
|
||||
$aNeg ? \substr($a, 1) : $a,
|
||||
$bNeg ? \substr($b, 1) : $b,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of a number.
|
||||
*
|
||||
* @param string $n The number.
|
||||
*
|
||||
* @return string The absolute value.
|
||||
*/
|
||||
final public function abs(string $n) : string
|
||||
{
|
||||
return ($n[0] === '-') ? \substr($n, 1) : $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates a number.
|
||||
*
|
||||
* @param string $n The number.
|
||||
*
|
||||
* @return string The negated value.
|
||||
*/
|
||||
final public function neg(string $n) : string
|
||||
{
|
||||
if ($n === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($n[0] === '-') {
|
||||
return \substr($n, 1);
|
||||
}
|
||||
|
||||
return '-' . $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two numbers.
|
||||
*
|
||||
* @param string $a The first number.
|
||||
* @param string $b The second number.
|
||||
*
|
||||
* @return int [-1, 0, 1] If the first number is less than, equal to, or greater than the second number.
|
||||
*/
|
||||
final public function cmp(string $a, string $b) : int
|
||||
{
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
if ($aNeg && ! $bNeg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($bNeg && ! $aNeg) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$aLen = \strlen($aDig);
|
||||
$bLen = \strlen($bDig);
|
||||
|
||||
if ($aLen < $bLen) {
|
||||
$result = -1;
|
||||
} elseif ($aLen > $bLen) {
|
||||
$result = 1;
|
||||
} else {
|
||||
$result = $aDig <=> $bDig;
|
||||
}
|
||||
|
||||
return $aNeg ? -$result : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two numbers.
|
||||
*
|
||||
* @param string $a The augend.
|
||||
* @param string $b The addend.
|
||||
*
|
||||
* @return string The sum.
|
||||
*/
|
||||
abstract public function add(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Subtracts two numbers.
|
||||
*
|
||||
* @param string $a The minuend.
|
||||
* @param string $b The subtrahend.
|
||||
*
|
||||
* @return string The difference.
|
||||
*/
|
||||
abstract public function sub(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Multiplies two numbers.
|
||||
*
|
||||
* @param string $a The multiplicand.
|
||||
* @param string $b The multiplier.
|
||||
*
|
||||
* @return string The product.
|
||||
*/
|
||||
abstract public function mul(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of two numbers.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
*
|
||||
* @return string The quotient.
|
||||
*/
|
||||
abstract public function divQ(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of two numbers.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
*
|
||||
* @return string The remainder.
|
||||
*/
|
||||
abstract public function divR(string $a, string $b) : string;
|
||||
|
||||
/**
|
||||
* Returns the quotient and remainder of the division of two numbers.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
*
|
||||
* @return string[] An array containing the quotient and remainder.
|
||||
*/
|
||||
abstract public function divQR(string $a, string $b) : array;
|
||||
|
||||
/**
|
||||
* Exponentiates a number.
|
||||
*
|
||||
* @param string $a The base number.
|
||||
* @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
|
||||
*
|
||||
* @return string The power.
|
||||
*/
|
||||
abstract public function pow(string $a, int $e) : string;
|
||||
|
||||
/**
|
||||
* @param string $a
|
||||
* @param string $b The modulus; must not be zero.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mod(string $a, string $b) : string
|
||||
{
|
||||
return $this->divR($this->add($this->divR($a, $b), $b), $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modular multiplicative inverse of $x modulo $m.
|
||||
*
|
||||
* If $x has no multiplicative inverse mod m, this method must return null.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library has built-in support.
|
||||
*
|
||||
* @param string $x
|
||||
* @param string $m The modulus; must not be negative or zero.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function modInverse(string $x, string $m) : ?string
|
||||
{
|
||||
if ($m === '1') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$modVal = $x;
|
||||
|
||||
if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
|
||||
$modVal = $this->mod($x, $m);
|
||||
}
|
||||
|
||||
$x = '0';
|
||||
$y = '0';
|
||||
$g = $this->gcdExtended($modVal, $m, $x, $y);
|
||||
|
||||
if ($g !== '1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->mod($this->add($this->mod($x, $m), $m), $m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raises a number into power with modulo.
|
||||
*
|
||||
* @param string $base The base number; must be positive or zero.
|
||||
* @param string $exp The exponent; must be positive or zero.
|
||||
* @param string $mod The modulus; must be strictly positive.
|
||||
*
|
||||
* @return string The power.
|
||||
*/
|
||||
abstract public function modPow(string $base, string $exp, string $mod) : string;
|
||||
|
||||
/**
|
||||
* Returns the greatest common divisor of the two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for GCD calculations.
|
||||
*
|
||||
* @param string $a The first number.
|
||||
* @param string $b The second number.
|
||||
*
|
||||
* @return string The GCD, always positive, or zero if both arguments are zero.
|
||||
*/
|
||||
public function gcd(string $a, string $b) : string
|
||||
{
|
||||
if ($a === '0') {
|
||||
return $this->abs($b);
|
||||
}
|
||||
|
||||
if ($b === '0') {
|
||||
return $this->abs($a);
|
||||
}
|
||||
|
||||
return $this->gcd($b, $this->divR($a, $b));
|
||||
}
|
||||
|
||||
private function gcdExtended(string $a, string $b, string &$x, string &$y) : string
|
||||
{
|
||||
if ($a === '0') {
|
||||
$x = '0';
|
||||
$y = '1';
|
||||
|
||||
return $b;
|
||||
}
|
||||
|
||||
$x1 = '0';
|
||||
$y1 = '0';
|
||||
|
||||
$gcd = $this->gcdExtended($this->mod($b, $a), $a, $x1, $y1);
|
||||
|
||||
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
|
||||
$y = $x1;
|
||||
|
||||
return $gcd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the square root of the given number, rounded down.
|
||||
*
|
||||
* The result is the largest x such that x² ≤ n.
|
||||
* The input MUST NOT be negative.
|
||||
*
|
||||
* @param string $n The number.
|
||||
*
|
||||
* @return string The square root.
|
||||
*/
|
||||
abstract public function sqrt(string $n) : string;
|
||||
|
||||
/**
|
||||
* Converts a number from an arbitrary base.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for base conversion.
|
||||
*
|
||||
* @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
|
||||
* @param int $base The base of the number, validated from 2 to 36.
|
||||
*
|
||||
* @return string The converted number, following the Calculator conventions.
|
||||
*/
|
||||
public function fromBase(string $number, int $base) : string
|
||||
{
|
||||
return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number to an arbitrary base.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for base conversion.
|
||||
*
|
||||
* @param string $number The number to convert, following the Calculator conventions.
|
||||
* @param int $base The base to convert to, validated from 2 to 36.
|
||||
*
|
||||
* @return string The converted number, lowercase.
|
||||
*/
|
||||
public function toBase(string $number, int $base) : string
|
||||
{
|
||||
$negative = ($number[0] === '-');
|
||||
|
||||
if ($negative) {
|
||||
$number = \substr($number, 1);
|
||||
}
|
||||
|
||||
$number = $this->toArbitraryBase($number, self::ALPHABET, $base);
|
||||
|
||||
if ($negative) {
|
||||
return '-' . $number;
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
|
||||
*
|
||||
* @param string $number The number to convert, validated as a non-empty string,
|
||||
* containing only chars in the given alphabet/base.
|
||||
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
|
||||
* @param int $base The base of the number, validated from 2 to alphabet length.
|
||||
*
|
||||
* @return string The number in base 10, following the Calculator conventions.
|
||||
*/
|
||||
final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string
|
||||
{
|
||||
// remove leading "zeros"
|
||||
$number = \ltrim($number, $alphabet[0]);
|
||||
|
||||
if ($number === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// optimize for "one"
|
||||
if ($number === $alphabet[1]) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
$result = '0';
|
||||
$power = '1';
|
||||
|
||||
$base = (string) $base;
|
||||
|
||||
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
|
||||
$index = \strpos($alphabet, $number[$i]);
|
||||
|
||||
if ($index !== 0) {
|
||||
$result = $this->add($result, ($index === 1)
|
||||
? $power
|
||||
: $this->mul($power, (string) $index)
|
||||
);
|
||||
}
|
||||
|
||||
if ($i !== 0) {
|
||||
$power = $this->mul($power, $base);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a non-negative number to an arbitrary base using a custom alphabet.
|
||||
*
|
||||
* @param string $number The number to convert, positive or zero, following the Calculator conventions.
|
||||
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
|
||||
* @param int $base The base to convert to, validated from 2 to alphabet length.
|
||||
*
|
||||
* @return string The converted number in the given alphabet.
|
||||
*/
|
||||
final public function toArbitraryBase(string $number, string $alphabet, int $base) : string
|
||||
{
|
||||
if ($number === '0') {
|
||||
return $alphabet[0];
|
||||
}
|
||||
|
||||
$base = (string) $base;
|
||||
$result = '';
|
||||
|
||||
while ($number !== '0') {
|
||||
[$number, $remainder] = $this->divQR($number, $base);
|
||||
$remainder = (int) $remainder;
|
||||
|
||||
$result .= $alphabet[$remainder];
|
||||
}
|
||||
|
||||
return \strrev($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a rounded division.
|
||||
*
|
||||
* Rounding is performed when the remainder of the division is not zero.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
* @param int $roundingMode The rounding mode.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \InvalidArgumentException If the rounding mode is invalid.
|
||||
* @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
|
||||
*/
|
||||
final public function divRound(string $a, string $b, int $roundingMode) : string
|
||||
{
|
||||
[$quotient, $remainder] = $this->divQR($a, $b);
|
||||
|
||||
$hasDiscardedFraction = ($remainder !== '0');
|
||||
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
|
||||
|
||||
$discardedFractionSign = function() use ($remainder, $b) : int {
|
||||
$r = $this->abs($this->mul($remainder, '2'));
|
||||
$b = $this->abs($b);
|
||||
|
||||
return $this->cmp($r, $b);
|
||||
};
|
||||
|
||||
$increment = false;
|
||||
|
||||
switch ($roundingMode) {
|
||||
case RoundingMode::UNNECESSARY:
|
||||
if ($hasDiscardedFraction) {
|
||||
throw RoundingNecessaryException::roundingNecessary();
|
||||
}
|
||||
break;
|
||||
|
||||
case RoundingMode::UP:
|
||||
$increment = $hasDiscardedFraction;
|
||||
break;
|
||||
|
||||
case RoundingMode::DOWN:
|
||||
break;
|
||||
|
||||
case RoundingMode::CEILING:
|
||||
$increment = $hasDiscardedFraction && $isPositiveOrZero;
|
||||
break;
|
||||
|
||||
case RoundingMode::FLOOR:
|
||||
$increment = $hasDiscardedFraction && ! $isPositiveOrZero;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_UP:
|
||||
$increment = $discardedFractionSign() >= 0;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_DOWN:
|
||||
$increment = $discardedFractionSign() > 0;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_CEILING:
|
||||
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_FLOOR:
|
||||
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||
break;
|
||||
|
||||
case RoundingMode::HALF_EVEN:
|
||||
$lastDigit = (int) $quotient[-1];
|
||||
$lastDigitIsEven = ($lastDigit % 2 === 0);
|
||||
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid rounding mode.');
|
||||
}
|
||||
|
||||
if ($increment) {
|
||||
return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
|
||||
}
|
||||
|
||||
return $quotient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise AND of two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for bitwise operations.
|
||||
*
|
||||
* @param string $a
|
||||
* @param string $b
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function and(string $a, string $b) : string
|
||||
{
|
||||
return $this->bitwise('and', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise OR of two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for bitwise operations.
|
||||
*
|
||||
* @param string $a
|
||||
* @param string $b
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function or(string $a, string $b) : string
|
||||
{
|
||||
return $this->bitwise('or', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise XOR of two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for bitwise operations.
|
||||
*
|
||||
* @param string $a
|
||||
* @param string $b
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function xor(string $a, string $b) : string
|
||||
{
|
||||
return $this->bitwise('xor', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bitwise operation on a decimal number.
|
||||
*
|
||||
* @param string $operator The operator to use, must be "and", "or" or "xor".
|
||||
* @param string $a The left operand.
|
||||
* @param string $b The right operand.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function bitwise(string $operator, string $a, string $b) : string
|
||||
{
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
$aBin = $this->toBinary($aDig);
|
||||
$bBin = $this->toBinary($bDig);
|
||||
|
||||
$aLen = \strlen($aBin);
|
||||
$bLen = \strlen($bBin);
|
||||
|
||||
if ($aLen > $bLen) {
|
||||
$bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin;
|
||||
} elseif ($bLen > $aLen) {
|
||||
$aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin;
|
||||
}
|
||||
|
||||
if ($aNeg) {
|
||||
$aBin = $this->twosComplement($aBin);
|
||||
}
|
||||
if ($bNeg) {
|
||||
$bBin = $this->twosComplement($bBin);
|
||||
}
|
||||
|
||||
switch ($operator) {
|
||||
case 'and':
|
||||
$value = $aBin & $bBin;
|
||||
$negative = ($aNeg and $bNeg);
|
||||
break;
|
||||
|
||||
case 'or':
|
||||
$value = $aBin | $bBin;
|
||||
$negative = ($aNeg or $bNeg);
|
||||
break;
|
||||
|
||||
case 'xor':
|
||||
$value = $aBin ^ $bBin;
|
||||
$negative = ($aNeg xor $bNeg);
|
||||
break;
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid bitwise operator.');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if ($negative) {
|
||||
$value = $this->twosComplement($value);
|
||||
}
|
||||
|
||||
$result = $this->toDecimal($value);
|
||||
|
||||
return $negative ? $this->neg($result) : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress InvalidOperand
|
||||
* @see https://github.com/vimeo/psalm/issues/4456
|
||||
*
|
||||
* @param string $number A positive, binary number.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function twosComplement(string $number) : string
|
||||
{
|
||||
$xor = \str_repeat("\xff", \strlen($number));
|
||||
|
||||
$number ^= $xor;
|
||||
|
||||
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
|
||||
$byte = \ord($number[$i]);
|
||||
|
||||
if (++$byte !== 256) {
|
||||
$number[$i] = \chr($byte);
|
||||
break;
|
||||
}
|
||||
|
||||
$number[$i] = "\x00";
|
||||
|
||||
if ($i === 0) {
|
||||
$number = "\x01" . $number;
|
||||
}
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a decimal number to a binary string.
|
||||
*
|
||||
* @param string $number The number to convert, positive or zero, only digits.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function toBinary(string $number) : string
|
||||
{
|
||||
$result = '';
|
||||
|
||||
while ($number !== '0') {
|
||||
[$number, $remainder] = $this->divQR($number, '256');
|
||||
$result .= \chr((int) $remainder);
|
||||
}
|
||||
|
||||
return \strrev($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the positive decimal representation of a binary number.
|
||||
*
|
||||
* @param string $bytes The bytes representing the number.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function toDecimal(string $bytes) : string
|
||||
{
|
||||
$result = '0';
|
||||
$power = '1';
|
||||
|
||||
for ($i = \strlen($bytes) - 1; $i >= 0; $i--) {
|
||||
$index = \ord($bytes[$i]);
|
||||
|
||||
if ($index !== 0) {
|
||||
$result = $this->add($result, ($index === 1)
|
||||
? $power
|
||||
: $this->mul($power, (string) $index)
|
||||
);
|
||||
}
|
||||
|
||||
if ($i !== 0) {
|
||||
$power = $this->mul($power, '256');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the bcmath library.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class BcMathCalculator extends Calculator
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function add(string $a, string $b) : string
|
||||
{
|
||||
return \bcadd($a, $b, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function sub(string $a, string $b) : string
|
||||
{
|
||||
return \bcsub($a, $b, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function mul(string $a, string $b) : string
|
||||
{
|
||||
return \bcmul($a, $b, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @psalm-suppress InvalidNullableReturnType
|
||||
* @psalm-suppress NullableReturnStatement
|
||||
*/
|
||||
public function divQ(string $a, string $b) : string
|
||||
{
|
||||
return \bcdiv($a, $b, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @psalm-suppress InvalidNullableReturnType
|
||||
* @psalm-suppress NullableReturnStatement
|
||||
*/
|
||||
public function divR(string $a, string $b) : string
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '7.2') >= 0) {
|
||||
return \bcmod($a, $b, 0);
|
||||
}
|
||||
|
||||
return \bcmod($a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function divQR(string $a, string $b) : array
|
||||
{
|
||||
$q = \bcdiv($a, $b, 0);
|
||||
|
||||
if (version_compare(PHP_VERSION, '7.2') >= 0) {
|
||||
$r = \bcmod($a, $b, 0);
|
||||
} else {
|
||||
$r = \bcmod($a, $b);
|
||||
}
|
||||
|
||||
assert($q !== null);
|
||||
assert($r !== null);
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function pow(string $a, int $e) : string
|
||||
{
|
||||
return \bcpow($a, (string) $e, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @psalm-suppress InvalidNullableReturnType
|
||||
* @psalm-suppress NullableReturnStatement
|
||||
*/
|
||||
public function modPow(string $base, string $exp, string $mod) : string
|
||||
{
|
||||
return \bcpowmod($base, $exp, $mod, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress NullableReturnStatement
|
||||
* @psalm-suppress InvalidNullableReturnType
|
||||
*/
|
||||
public function sqrt(string $n) : string
|
||||
{
|
||||
return \bcsqrt($n, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the GMP library.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class GmpCalculator extends Calculator
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function add(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_add($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function sub(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_sub($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function mul(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_mul($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function divQ(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_div_q($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function divR(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_div_r($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function divQR(string $a, string $b) : array
|
||||
{
|
||||
[$q, $r] = \gmp_div_qr($a, $b);
|
||||
|
||||
return [
|
||||
\gmp_strval($q),
|
||||
\gmp_strval($r)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function pow(string $a, int $e) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_pow($a, $e));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function modInverse(string $x, string $m) : ?string
|
||||
{
|
||||
$result = \gmp_invert($x, $m);
|
||||
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \gmp_strval($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function modPow(string $base, string $exp, string $mod) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_powm($base, $exp, $mod));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function gcd(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_gcd($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fromBase(string $number, int $base) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_init($number, $base));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toBase(string $number, int $base) : string
|
||||
{
|
||||
return \gmp_strval($number, $base);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function and(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_and($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function or(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_or($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function xor(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_xor($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function sqrt(string $n) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_sqrt($n));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
|
||||
/**
|
||||
* Calculator implementation using only native PHP code.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class NativeCalculator extends Calculator
|
||||
{
|
||||
/**
|
||||
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
|
||||
* For multiplication, this represents the max sum of the lengths of both operands.
|
||||
*
|
||||
* For addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
|
||||
* Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
|
||||
* 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $maxDigits;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
switch (PHP_INT_SIZE) {
|
||||
case 4:
|
||||
$this->maxDigits = 9;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
$this->maxDigits = 18;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function add(string $a, string $b) : string
|
||||
{
|
||||
/**
|
||||
* @psalm-var numeric-string $a
|
||||
* @psalm-var numeric-string $b
|
||||
*/
|
||||
$result = $a + $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return (string) $result;
|
||||
}
|
||||
|
||||
if ($a === '0') {
|
||||
return $b;
|
||||
}
|
||||
|
||||
if ($b === '0') {
|
||||
return $a;
|
||||
}
|
||||
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
$result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
|
||||
|
||||
if ($aNeg) {
|
||||
$result = $this->neg($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function sub(string $a, string $b) : string
|
||||
{
|
||||
return $this->add($a, $this->neg($b));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function mul(string $a, string $b) : string
|
||||
{
|
||||
/**
|
||||
* @psalm-var numeric-string $a
|
||||
* @psalm-var numeric-string $b
|
||||
*/
|
||||
$result = $a * $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return (string) $result;
|
||||
}
|
||||
|
||||
if ($a === '0' || $b === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($a === '1') {
|
||||
return $b;
|
||||
}
|
||||
|
||||
if ($b === '1') {
|
||||
return $a;
|
||||
}
|
||||
|
||||
if ($a === '-1') {
|
||||
return $this->neg($b);
|
||||
}
|
||||
|
||||
if ($b === '-1') {
|
||||
return $this->neg($a);
|
||||
}
|
||||
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
$result = $this->doMul($aDig, $bDig);
|
||||
|
||||
if ($aNeg !== $bNeg) {
|
||||
$result = $this->neg($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function divQ(string $a, string $b) : string
|
||||
{
|
||||
return $this->divQR($a, $b)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function divR(string $a, string $b): string
|
||||
{
|
||||
return $this->divQR($a, $b)[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function divQR(string $a, string $b) : array
|
||||
{
|
||||
if ($a === '0') {
|
||||
return ['0', '0'];
|
||||
}
|
||||
|
||||
if ($a === $b) {
|
||||
return ['1', '0'];
|
||||
}
|
||||
|
||||
if ($b === '1') {
|
||||
return [$a, '0'];
|
||||
}
|
||||
|
||||
if ($b === '-1') {
|
||||
return [$this->neg($a), '0'];
|
||||
}
|
||||
|
||||
/** @psalm-var numeric-string $a */
|
||||
$na = $a * 1; // cast to number
|
||||
|
||||
if (is_int($na)) {
|
||||
/** @psalm-var numeric-string $b */
|
||||
$nb = $b * 1;
|
||||
|
||||
if (is_int($nb)) {
|
||||
// the only division that may overflow is PHP_INT_MIN / -1,
|
||||
// which cannot happen here as we've already handled a divisor of -1 above.
|
||||
$r = $na % $nb;
|
||||
$q = ($na - $r) / $nb;
|
||||
|
||||
assert(is_int($q));
|
||||
|
||||
return [
|
||||
(string) $q,
|
||||
(string) $r
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
[$q, $r] = $this->doDiv($aDig, $bDig);
|
||||
|
||||
if ($aNeg !== $bNeg) {
|
||||
$q = $this->neg($q);
|
||||
}
|
||||
|
||||
if ($aNeg) {
|
||||
$r = $this->neg($r);
|
||||
}
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function pow(string $a, int $e) : string
|
||||
{
|
||||
if ($e === 0) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
if ($e === 1) {
|
||||
return $a;
|
||||
}
|
||||
|
||||
$odd = $e % 2;
|
||||
$e -= $odd;
|
||||
|
||||
$aa = $this->mul($a, $a);
|
||||
|
||||
/** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */
|
||||
$result = $this->pow($aa, $e / 2);
|
||||
|
||||
if ($odd === 1) {
|
||||
$result = $this->mul($result, $a);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function modPow(string $base, string $exp, string $mod) : string
|
||||
{
|
||||
// special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0)
|
||||
if ($base === '0' && $exp === '0' && $mod === '1') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
|
||||
if ($exp === '0' && $mod === '1') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$x = $base;
|
||||
|
||||
$res = '1';
|
||||
|
||||
// numbers are positive, so we can use remainder instead of modulo
|
||||
$x = $this->divR($x, $mod);
|
||||
|
||||
while ($exp !== '0') {
|
||||
if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
|
||||
$res = $this->divR($this->mul($res, $x), $mod);
|
||||
}
|
||||
|
||||
$exp = $this->divQ($exp, '2');
|
||||
$x = $this->divR($this->mul($x, $x), $mod);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function sqrt(string $n) : string
|
||||
{
|
||||
if ($n === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// initial approximation
|
||||
$x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1);
|
||||
|
||||
$decreased = false;
|
||||
|
||||
for (;;) {
|
||||
$nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
|
||||
|
||||
if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
|
||||
break;
|
||||
}
|
||||
|
||||
$decreased = $this->cmp($nx, $x) < 0;
|
||||
$x = $nx;
|
||||
}
|
||||
|
||||
return $x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the addition of two non-signed large integers.
|
||||
*
|
||||
* @param string $a The first operand.
|
||||
* @param string $b The second operand.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function doAdd(string $a, string $b) : string
|
||||
{
|
||||
[$a, $b, $length] = $this->pad($a, $b);
|
||||
|
||||
$carry = 0;
|
||||
$result = '';
|
||||
|
||||
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
|
||||
$blockLength = $this->maxDigits;
|
||||
|
||||
if ($i < 0) {
|
||||
$blockLength += $i;
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
/** @psalm-var numeric-string $blockA */
|
||||
$blockA = \substr($a, $i, $blockLength);
|
||||
|
||||
/** @psalm-var numeric-string $blockB */
|
||||
$blockB = \substr($b, $i, $blockLength);
|
||||
|
||||
$sum = (string) ($blockA + $blockB + $carry);
|
||||
$sumLength = \strlen($sum);
|
||||
|
||||
if ($sumLength > $blockLength) {
|
||||
$sum = \substr($sum, 1);
|
||||
$carry = 1;
|
||||
} else {
|
||||
if ($sumLength < $blockLength) {
|
||||
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
|
||||
}
|
||||
$carry = 0;
|
||||
}
|
||||
|
||||
$result = $sum . $result;
|
||||
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($carry === 1) {
|
||||
$result = '1' . $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the subtraction of two non-signed large integers.
|
||||
*
|
||||
* @param string $a The first operand.
|
||||
* @param string $b The second operand.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function doSub(string $a, string $b) : string
|
||||
{
|
||||
if ($a === $b) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// Ensure that we always subtract to a positive result: biggest minus smallest.
|
||||
$cmp = $this->doCmp($a, $b);
|
||||
|
||||
$invert = ($cmp === -1);
|
||||
|
||||
if ($invert) {
|
||||
$c = $a;
|
||||
$a = $b;
|
||||
$b = $c;
|
||||
}
|
||||
|
||||
[$a, $b, $length] = $this->pad($a, $b);
|
||||
|
||||
$carry = 0;
|
||||
$result = '';
|
||||
|
||||
$complement = 10 ** $this->maxDigits;
|
||||
|
||||
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
|
||||
$blockLength = $this->maxDigits;
|
||||
|
||||
if ($i < 0) {
|
||||
$blockLength += $i;
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
/** @psalm-var numeric-string $blockA */
|
||||
$blockA = \substr($a, $i, $blockLength);
|
||||
|
||||
/** @psalm-var numeric-string $blockB */
|
||||
$blockB = \substr($b, $i, $blockLength);
|
||||
|
||||
$sum = $blockA - $blockB - $carry;
|
||||
|
||||
if ($sum < 0) {
|
||||
$sum += $complement;
|
||||
$carry = 1;
|
||||
} else {
|
||||
$carry = 0;
|
||||
}
|
||||
|
||||
$sum = (string) $sum;
|
||||
$sumLength = \strlen($sum);
|
||||
|
||||
if ($sumLength < $blockLength) {
|
||||
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
|
||||
}
|
||||
|
||||
$result = $sum . $result;
|
||||
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Carry cannot be 1 when the loop ends, as a > b
|
||||
assert($carry === 0);
|
||||
|
||||
$result = \ltrim($result, '0');
|
||||
|
||||
if ($invert) {
|
||||
$result = $this->neg($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the multiplication of two non-signed large integers.
|
||||
*
|
||||
* @param string $a The first operand.
|
||||
* @param string $b The second operand.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function doMul(string $a, string $b) : string
|
||||
{
|
||||
$x = \strlen($a);
|
||||
$y = \strlen($b);
|
||||
|
||||
$maxDigits = \intdiv($this->maxDigits, 2);
|
||||
$complement = 10 ** $maxDigits;
|
||||
|
||||
$result = '0';
|
||||
|
||||
for ($i = $x - $maxDigits;; $i -= $maxDigits) {
|
||||
$blockALength = $maxDigits;
|
||||
|
||||
if ($i < 0) {
|
||||
$blockALength += $i;
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
$blockA = (int) \substr($a, $i, $blockALength);
|
||||
|
||||
$line = '';
|
||||
$carry = 0;
|
||||
|
||||
for ($j = $y - $maxDigits;; $j -= $maxDigits) {
|
||||
$blockBLength = $maxDigits;
|
||||
|
||||
if ($j < 0) {
|
||||
$blockBLength += $j;
|
||||
/** @psalm-suppress LoopInvalidation */
|
||||
$j = 0;
|
||||
}
|
||||
|
||||
$blockB = (int) \substr($b, $j, $blockBLength);
|
||||
|
||||
$mul = $blockA * $blockB + $carry;
|
||||
$value = $mul % $complement;
|
||||
$carry = ($mul - $value) / $complement;
|
||||
|
||||
$value = (string) $value;
|
||||
$value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
|
||||
|
||||
$line = $value . $line;
|
||||
|
||||
if ($j === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($carry !== 0) {
|
||||
$line = $carry . $line;
|
||||
}
|
||||
|
||||
$line = \ltrim($line, '0');
|
||||
|
||||
if ($line !== '') {
|
||||
$line .= \str_repeat('0', $x - $blockALength - $i);
|
||||
$result = $this->add($result, $line);
|
||||
}
|
||||
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the division of two non-signed large integers.
|
||||
*
|
||||
* @param string $a The first operand.
|
||||
* @param string $b The second operand.
|
||||
*
|
||||
* @return string[] The quotient and remainder.
|
||||
*/
|
||||
private function doDiv(string $a, string $b) : array
|
||||
{
|
||||
$cmp = $this->doCmp($a, $b);
|
||||
|
||||
if ($cmp === -1) {
|
||||
return ['0', $a];
|
||||
}
|
||||
|
||||
$x = \strlen($a);
|
||||
$y = \strlen($b);
|
||||
|
||||
// we now know that a >= b && x >= y
|
||||
|
||||
$q = '0'; // quotient
|
||||
$r = $a; // remainder
|
||||
$z = $y; // focus length, always $y or $y+1
|
||||
|
||||
for (;;) {
|
||||
$focus = \substr($a, 0, $z);
|
||||
|
||||
$cmp = $this->doCmp($focus, $b);
|
||||
|
||||
if ($cmp === -1) {
|
||||
if ($z === $x) { // remainder < dividend
|
||||
break;
|
||||
}
|
||||
|
||||
$z++;
|
||||
}
|
||||
|
||||
$zeros = \str_repeat('0', $x - $z);
|
||||
|
||||
$q = $this->add($q, '1' . $zeros);
|
||||
$a = $this->sub($a, $b . $zeros);
|
||||
|
||||
$r = $a;
|
||||
|
||||
if ($r === '0') { // remainder == 0
|
||||
break;
|
||||
}
|
||||
|
||||
$x = \strlen($a);
|
||||
|
||||
if ($x < $y) { // remainder < dividend
|
||||
break;
|
||||
}
|
||||
|
||||
$z = $y;
|
||||
}
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two non-signed large numbers.
|
||||
*
|
||||
* @param string $a The first operand.
|
||||
* @param string $b The second operand.
|
||||
*
|
||||
* @return int [-1, 0, 1]
|
||||
*/
|
||||
private function doCmp(string $a, string $b) : int
|
||||
{
|
||||
$x = \strlen($a);
|
||||
$y = \strlen($b);
|
||||
|
||||
$cmp = $x <=> $y;
|
||||
|
||||
if ($cmp !== 0) {
|
||||
return $cmp;
|
||||
}
|
||||
|
||||
return \strcmp($a, $b) <=> 0; // enforce [-1, 0, 1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
|
||||
*
|
||||
* The numbers must only consist of digits, without leading minus sign.
|
||||
*
|
||||
* @param string $a The first operand.
|
||||
* @param string $b The second operand.
|
||||
*
|
||||
* @return array{0: string, 1: string, 2: int}
|
||||
*/
|
||||
private function pad(string $a, string $b) : array
|
||||
{
|
||||
$x = \strlen($a);
|
||||
$y = \strlen($b);
|
||||
|
||||
if ($x > $y) {
|
||||
$b = \str_repeat('0', $x - $y) . $b;
|
||||
|
||||
return [$a, $b, $x];
|
||||
}
|
||||
|
||||
if ($x < $y) {
|
||||
$a = \str_repeat('0', $y - $x) . $a;
|
||||
|
||||
return [$a, $b, $y];
|
||||
}
|
||||
|
||||
return [$a, $b, $x];
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
/**
|
||||
* Specifies a rounding behavior for numerical operations capable of discarding precision.
|
||||
*
|
||||
* Each rounding mode indicates how the least significant returned digit of a rounded result
|
||||
* is to be calculated. If fewer digits are returned than the digits needed to represent the
|
||||
* exact numerical result, the discarded digits will be referred to as the discarded fraction
|
||||
* regardless the digits' contribution to the value of the number. In other words, considered
|
||||
* as a numerical value, the discarded fraction could have an absolute value greater than one.
|
||||
*/
|
||||
final class RoundingMode
|
||||
{
|
||||
/**
|
||||
* Private constructor. This class is not instantiable.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the requested operation has an exact result, hence no rounding is necessary.
|
||||
*
|
||||
* If this rounding mode is specified on an operation that yields a result that
|
||||
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
|
||||
*/
|
||||
public const UNNECESSARY = 0;
|
||||
|
||||
/**
|
||||
* Rounds away from zero.
|
||||
*
|
||||
* Always increments the digit prior to a nonzero discarded fraction.
|
||||
* Note that this rounding mode never decreases the magnitude of the calculated value.
|
||||
*/
|
||||
public const UP = 1;
|
||||
|
||||
/**
|
||||
* Rounds towards zero.
|
||||
*
|
||||
* Never increments the digit prior to a discarded fraction (i.e., truncates).
|
||||
* Note that this rounding mode never increases the magnitude of the calculated value.
|
||||
*/
|
||||
public const DOWN = 2;
|
||||
|
||||
/**
|
||||
* Rounds towards positive infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for UP; if negative, behaves as for DOWN.
|
||||
* Note that this rounding mode never decreases the calculated value.
|
||||
*/
|
||||
public const CEILING = 3;
|
||||
|
||||
/**
|
||||
* Rounds towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behave as for DOWN; if negative, behave as for UP.
|
||||
* Note that this rounding mode never increases the calculated value.
|
||||
*/
|
||||
public const FLOOR = 4;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
|
||||
*
|
||||
* Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN.
|
||||
* Note that this is the rounding mode commonly taught at school.
|
||||
*/
|
||||
public const HALF_UP = 5;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
|
||||
*
|
||||
* Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN.
|
||||
*/
|
||||
public const HALF_DOWN = 6;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN.
|
||||
*/
|
||||
public const HALF_CEILING = 7;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP.
|
||||
*/
|
||||
public const HALF_FLOOR = 8;
|
||||
|
||||
/**
|
||||
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
|
||||
*
|
||||
* Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd;
|
||||
* behaves as for HALF_DOWN if it's even.
|
||||
*
|
||||
* Note that this is the rounding mode that statistically minimizes
|
||||
* cumulative error when applied repeatedly over a sequence of calculations.
|
||||
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
|
||||
*/
|
||||
public const HALF_EVEN = 9;
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the last two release streams are supported.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.9.x | :white_check_mark: |
|
||||
| 0.8.x | :white_check_mark: |
|
||||
| < 0.8 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a security vulnerability, please use the
|
||||
[Tidelift security contact](https://tidelift.com/security).
|
||||
Tidelift will coordinate the fix and disclosure.
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "brick/math",
|
||||
"description": "Arbitrary-precision arithmetic library",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"Brick",
|
||||
"Math",
|
||||
"Arbitrary-precision",
|
||||
"Arithmetic",
|
||||
"BigInteger",
|
||||
"BigDecimal",
|
||||
"BigRational",
|
||||
"Bignum"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0",
|
||||
"php-coveralls/php-coveralls": "^2.2",
|
||||
"vimeo/psalm": "4.4.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Brick\\Math\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Brick\\Math\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This script stress tests calculators with random large numbers and ensures that all implementations return the same
|
||||
* results. It is designed to run in an infinite loop unless a bug is found.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
|
||||
(new class(30) { // max digits
|
||||
private $gmp;
|
||||
private $bcmath;
|
||||
private $native;
|
||||
|
||||
private $maxDigits;
|
||||
|
||||
public function __construct(int $maxDigits)
|
||||
{
|
||||
$this->gmp = new Calculator\GmpCalculator();
|
||||
$this->bcmath = new Calculator\BcMathCalculator();
|
||||
$this->native = new Calculator\NativeCalculator();
|
||||
|
||||
$this->maxDigits = $maxDigits;
|
||||
}
|
||||
|
||||
public function __invoke() : void
|
||||
{
|
||||
for (;;) {
|
||||
$a = $this->generateRandomNumber();
|
||||
$b = $this->generateRandomNumber();
|
||||
$c = $this->generateRandomNumber();
|
||||
|
||||
$this->runTests($a, $b);
|
||||
$this->runTests($b, $a);
|
||||
|
||||
if ($a !== '0') {
|
||||
$this->runTests("-$a", $b);
|
||||
$this->runTests($b, "-$a");
|
||||
}
|
||||
|
||||
if ($b !== '0') {
|
||||
$this->runTests($a, "-$b");
|
||||
$this->runTests("-$b", $a);
|
||||
}
|
||||
|
||||
if ($a !== '0' && $b !== '0') {
|
||||
$this->runTests("-$a", "-$b");
|
||||
$this->runTests("-$b", "-$a");
|
||||
}
|
||||
|
||||
if ($c !== '0') {
|
||||
$this->test("$a POW $b MOD $c", function(Calculator $calc) use($a, $b, $c) {
|
||||
return $calc->modPow($a, $b, $c);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $a The left operand.
|
||||
* @param string $b The right operand.
|
||||
*/
|
||||
private function runTests(string $a, string $b) : void
|
||||
{
|
||||
$this->test("$a + $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->add($a, $b);
|
||||
});
|
||||
|
||||
$this->test("$a - $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->sub($a, $b);
|
||||
});
|
||||
|
||||
$this->test("$a * $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->mul($a, $b);
|
||||
});
|
||||
|
||||
if ($b !== '0') {
|
||||
$this->test("$a / $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->divQR($a, $b);
|
||||
});
|
||||
|
||||
$this->test("$a MOD $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->mod($a, $b);
|
||||
});
|
||||
}
|
||||
|
||||
if ($b !== '0' && $b[0] !== '-') {
|
||||
$this->test("INV $a MOD $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->modInverse($a, $b);
|
||||
});
|
||||
}
|
||||
|
||||
$this->test("GCD $a, $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->gcd($a, $b);
|
||||
});
|
||||
|
||||
if ($a[0] !== '-') {
|
||||
$this->test("SQRT $a", function(Calculator $c) use($a, $b) {
|
||||
return $c->sqrt($a);
|
||||
});
|
||||
}
|
||||
|
||||
$this->test("$a AND $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->and($a, $b);
|
||||
});
|
||||
|
||||
$this->test("$a OR $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->or($a, $b);
|
||||
});
|
||||
|
||||
$this->test("$a XOR $b", function(Calculator $c) use($a, $b) {
|
||||
return $c->xor($a, $b);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $test A string representing the test being executed.
|
||||
* @param Closure $callback A callback function accepting a Calculator instance and returning a calculation result.
|
||||
*/
|
||||
private function test(string $test, Closure $callback) : void
|
||||
{
|
||||
static $testCounter = 0;
|
||||
static $lastOutputTime = 0.0;
|
||||
static $currentSecond = 0;
|
||||
static $currentSecondTestCounter = 0;
|
||||
static $testsPerSecond = 0;
|
||||
|
||||
$gmpResult = $callback($this->gmp);
|
||||
$bcmathResult = $callback($this->bcmath);
|
||||
$nativeResult = $callback($this->native);
|
||||
|
||||
if ($gmpResult !== $bcmathResult) {
|
||||
self::failure('GMP', 'BCMath', $test);
|
||||
}
|
||||
|
||||
if ($gmpResult !== $nativeResult) {
|
||||
self::failure('GMP', 'Native', $test);
|
||||
}
|
||||
|
||||
$testCounter++;
|
||||
$currentSecondTestCounter++;
|
||||
|
||||
$time = microtime(true);
|
||||
$second = (int) $time;
|
||||
|
||||
if ($second !== $currentSecond) {
|
||||
$currentSecond = $second;
|
||||
$testsPerSecond = $currentSecondTestCounter;
|
||||
$currentSecondTestCounter = 0;
|
||||
}
|
||||
|
||||
if ($time - $lastOutputTime >= 0.1) {
|
||||
echo "\r", number_format($testCounter), ' (', number_format($testsPerSecond) . ' / s)';
|
||||
$lastOutputTime = $time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $c1 The name of the first calculator.
|
||||
* @param string $c2 The name of the second calculator.
|
||||
* @param string $test A string representing the test being executed.
|
||||
*/
|
||||
private static function failure(string $c1, string $c2, string $test) : void
|
||||
{
|
||||
echo PHP_EOL;
|
||||
echo 'FAILURE!', PHP_EOL;
|
||||
echo $c1, ' vs ', $c2, PHP_EOL;
|
||||
echo $test, PHP_EOL;
|
||||
die;
|
||||
}
|
||||
|
||||
private function generateRandomNumber() : string
|
||||
{
|
||||
$length = random_int(1, $this->maxDigits);
|
||||
|
||||
$number = '';
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$number .= random_int(0, 9);
|
||||
}
|
||||
|
||||
$number = ltrim($number, '0');
|
||||
|
||||
if ($number === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
})();
|
||||
Vendored
+23
-18
@@ -1,24 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2009, Robert Hafner
|
||||
All rights reserved.
|
||||
|
||||
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 Stash Project nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL Robert Hafner BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. 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.
|
||||
|
||||
3. 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.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+37
-16
@@ -38,6 +38,13 @@ class Minifier
|
||||
*/
|
||||
protected $input;
|
||||
|
||||
/**
|
||||
* Length of input javascript.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $len = 0;
|
||||
|
||||
/**
|
||||
* The location of the character (in the input string) that is next to be
|
||||
* processed.
|
||||
@@ -77,7 +84,7 @@ class Minifier
|
||||
/**
|
||||
* These characters are used to define strings.
|
||||
*/
|
||||
protected $stringDelimiters = ['\'', '"', '`'];
|
||||
protected $stringDelimiters = ['\'' => true, '"' => true, '`' => true];
|
||||
|
||||
/**
|
||||
* Contains the default options for minification. This array is merged with
|
||||
@@ -86,7 +93,7 @@ class Minifier
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $defaultOptions = array('flaggedComments' => true);
|
||||
protected static $defaultOptions = ['flaggedComments' => true];
|
||||
|
||||
/**
|
||||
* Contains lock ids which are used to replace certain code patterns and
|
||||
@@ -94,7 +101,7 @@ class Minifier
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $locks = array();
|
||||
protected $locks = [];
|
||||
|
||||
/**
|
||||
* Takes a string containing javascript and removes unneeded characters in
|
||||
@@ -105,7 +112,7 @@ class Minifier
|
||||
* @throws \Exception
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function minify($js, $options = array())
|
||||
public static function minify($js, $options = [])
|
||||
{
|
||||
try {
|
||||
ob_start();
|
||||
@@ -157,21 +164,34 @@ class Minifier
|
||||
protected function initialize($js, $options)
|
||||
{
|
||||
$this->options = array_merge(static::$defaultOptions, $options);
|
||||
$js = str_replace("\r\n", "\n", $js);
|
||||
$js = str_replace('/**/', '', $js);
|
||||
$this->input = str_replace("\r", "\n", $js);
|
||||
$this->input = str_replace(["\r\n", '/**/', "\r"], ["\n", "", "\n"], $js);
|
||||
|
||||
// We add a newline to the end of the script to make it easier to deal
|
||||
// with comments at the bottom of the script- this prevents the unclosed
|
||||
// comment error that can otherwise occur.
|
||||
$this->input .= PHP_EOL;
|
||||
|
||||
// save input length to skip calculation every time
|
||||
$this->len = strlen($this->input);
|
||||
|
||||
// Populate "a" with a new line, "b" with the first character, before
|
||||
// entering the loop
|
||||
$this->a = "\n";
|
||||
$this->b = $this->getReal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Characters that can't stand alone preserve the newline.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $noNewLineCharacters = [
|
||||
'(' => true,
|
||||
'-' => true,
|
||||
'+' => true,
|
||||
'[' => true,
|
||||
'@' => true];
|
||||
|
||||
/**
|
||||
* The primary action occurs here. This function loops through the input string,
|
||||
* outputting anything that's relevant and discarding anything that is not.
|
||||
@@ -183,7 +203,7 @@ class Minifier
|
||||
// new lines
|
||||
case "\n":
|
||||
// if the next line is something that can't stand alone preserve the newline
|
||||
if ($this->b !== false && strpos('(-+[@', $this->b) !== false) {
|
||||
if ($this->b !== false && isset($this->noNewLineCharacters[$this->b])) {
|
||||
echo $this->a;
|
||||
$this->saveString();
|
||||
break;
|
||||
@@ -226,7 +246,7 @@ class Minifier
|
||||
break;
|
||||
}
|
||||
|
||||
// no break
|
||||
// no break
|
||||
default:
|
||||
// check for some regex that breaks stuff
|
||||
if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
|
||||
@@ -257,6 +277,7 @@ class Minifier
|
||||
protected function clean()
|
||||
{
|
||||
unset($this->input);
|
||||
$this->len = 0;
|
||||
$this->index = 0;
|
||||
$this->a = $this->b = '';
|
||||
unset($this->c);
|
||||
@@ -276,7 +297,7 @@ class Minifier
|
||||
unset($this->c);
|
||||
} else {
|
||||
// Otherwise we start pulling from the input.
|
||||
$char = substr($this->input, $this->index, 1);
|
||||
$char = $this->index < $this->len ? $this->input[$this->index] : false;
|
||||
|
||||
// If the next character doesn't exist return false.
|
||||
if (isset($char) && $char === false) {
|
||||
@@ -289,7 +310,7 @@ class Minifier
|
||||
|
||||
// Normalize all whitespace except for the newline character into a
|
||||
// standard space.
|
||||
if ($char !== "\n" && ord($char) < 32) {
|
||||
if ($char !== "\n" && $char < "\x20") {
|
||||
return ' ';
|
||||
}
|
||||
|
||||
@@ -340,7 +361,7 @@ class Minifier
|
||||
*/
|
||||
protected function processOneLineComments($startIndex)
|
||||
{
|
||||
$thirdCommentString = substr($this->input, $this->index, 1);
|
||||
$thirdCommentString = $this->index < $this->len ? $this->input[$this->index] : false;
|
||||
|
||||
// kill rest of line
|
||||
$this->getNext("\n");
|
||||
@@ -429,7 +450,7 @@ class Minifier
|
||||
$this->index = $pos;
|
||||
|
||||
// Return the first character of that string.
|
||||
return substr($this->input, $this->index, 1);
|
||||
return $this->index < $this->len ? $this->input[$this->index] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -447,7 +468,7 @@ class Minifier
|
||||
$this->a = $this->b;
|
||||
|
||||
// If this isn't a string we don't need to do anything.
|
||||
if (!in_array($this->a, $this->stringDelimiters)) {
|
||||
if (!isset($this->stringDelimiters[$this->a])) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -557,7 +578,7 @@ class Minifier
|
||||
/* lock things like <code>"asd" + ++x;</code> */
|
||||
$lock = '"LOCK---' . crc32(time()) . '"';
|
||||
|
||||
$matches = array();
|
||||
$matches = [];
|
||||
preg_match('/([+-])(\s+)([+-])/S', $js, $matches);
|
||||
if (empty($matches)) {
|
||||
return $js;
|
||||
@@ -589,4 +610,4 @@ class Minifier
|
||||
|
||||
return $js;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+12
-1
@@ -5,7 +5,6 @@
|
||||
[](https://coveralls.io/r/tedivm/JShrink?branch=master)
|
||||
[](https://packagist.org/packages/tedivm/JShrink)
|
||||
|
||||
|
||||
JShrink is a php class that minifies javascript so that it can be delivered to the client quicker. This code can be used
|
||||
by any product looking to minify their javascript on the fly (although caching the results is suggested for performance
|
||||
reasons). Unlike many other products this is not a port into php but a native application, resulting in better
|
||||
@@ -59,3 +58,15 @@ Releases of JShrink are available on [Github](https://github.com/tedious/JShrink
|
||||
JShrink is licensed under the BSD License. See the LICENSE file for details.
|
||||
|
||||
In the spirit of open source, use of this library for evil is discouraged but not prohibited.
|
||||
|
||||
|
||||
## Reporting Security and Vulnerability Issues
|
||||
|
||||
This project utilizes the [Tidelift Security Reporting System](https://tidelift.com/security) for security and vulnerability reporting.
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
Issues can be opened directly in Github for issues that aren't related to security.
|
||||
|
||||
[Professionally supported JShrink is now available with Tidelift.](https://tidelift.com/subscription/pkg/packagist-tedivm-jshrink?utm_source=packagist-tedivm-jshrink&utm_medium=referral&utm_campaign=readme)
|
||||
+4
-4
@@ -12,12 +12,12 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
"php": "^5.6|^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.0.*",
|
||||
"fabpot/php-cs-fixer": "0.4.0",
|
||||
"satooshi/php-coveralls": "dev-master"
|
||||
"phpunit/phpunit": "^6",
|
||||
"friendsofphp/php-cs-fixer": "^2.8",
|
||||
"php-coveralls/php-coveralls": "^1.1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {"JShrink": "src/"}
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit bootstrap="./tests/bootstrap.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="JShrink Test Suite">
|
||||
<directory suffix="Test.php">./tests/JShrink/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>requests</group>
|
||||
<group>development</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">./src/JShrink/</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<logging>
|
||||
<log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
|
||||
</logging>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@spomky-labs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2017 Spomky-Labs
|
||||
|
||||
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, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"name": "web-token/jwt-framework",
|
||||
"description": "JSON Object Signing and Encryption library for PHP and Symfony Bundle.",
|
||||
"type": "symfony-bundle",
|
||||
"license": "MIT",
|
||||
"keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"],
|
||||
"homepage": "https://github.com/web-token/jwt-framework",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Florent Morselli",
|
||||
"homepage": "https://github.com/Spomky"
|
||||
},{
|
||||
"name": "All contributors",
|
||||
"homepage": "https://github.com/web-token/jwt-framework/contributors"
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"test:unit": "./vendor/bin/phpunit --color",
|
||||
"test:typing": "./vendor/bin/phpstan analyse",
|
||||
"test:syntax": "./vendor/bin/php-cs-fixer fix --dry-run --stop-on-violation --using-cache=no",
|
||||
"test:mutations": "./vendor/bin/infection --logger-github --git-diff-filter=AM -s --threads=$(nproc) --min-msi=0 --min-covered-msi=0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jose\\": "src/",
|
||||
"Jose\\Component\\Signature\\Algorithm\\": [
|
||||
"src/SignatureAlgorithm/ECDSA",
|
||||
"src/SignatureAlgorithm/EdDSA",
|
||||
"src/SignatureAlgorithm/HMAC",
|
||||
"src/SignatureAlgorithm/None",
|
||||
"src/SignatureAlgorithm/RSA",
|
||||
"src/SignatureAlgorithm/Experimental"
|
||||
],
|
||||
"Jose\\Component\\Core\\Util\\Ecc\\": [
|
||||
"src/Ecc"
|
||||
],
|
||||
"Jose\\Component\\Encryption\\Algorithm\\": [
|
||||
"src/EncryptionAlgorithm/Experimental"
|
||||
],
|
||||
"Jose\\Component\\Encryption\\Algorithm\\KeyEncryption\\": [
|
||||
"src/EncryptionAlgorithm/KeyEncryption/AESGCMKW",
|
||||
"src/EncryptionAlgorithm/KeyEncryption/AESKW",
|
||||
"src/EncryptionAlgorithm/KeyEncryption/Direct",
|
||||
"src/EncryptionAlgorithm/KeyEncryption/ECDHES",
|
||||
"src/EncryptionAlgorithm/KeyEncryption/PBES2",
|
||||
"src/EncryptionAlgorithm/KeyEncryption/RSA"
|
||||
],
|
||||
"Jose\\Component\\Encryption\\Algorithm\\ContentEncryption\\": [
|
||||
"src/EncryptionAlgorithm/ContentEncryption/AESGCM",
|
||||
"src/EncryptionAlgorithm/ContentEncryption/AESCBC"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Jose\\Performance\\": "performance/",
|
||||
"Jose\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-sodium": "*",
|
||||
"brick/math": "^0.8.17|^0.9",
|
||||
"fgrosse/phpasn1": "^2.0",
|
||||
"psr/event-dispatcher": "^1.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"spomky-labs/aes-key-wrap": "^5.0|^6.0",
|
||||
"spomky-labs/base64url": "^1.0|^2.0",
|
||||
"symfony/config": "^4.2|^5.0",
|
||||
"symfony/console": "^4.2|^5.0",
|
||||
"symfony/dependency-injection": "^4.2|^5.0",
|
||||
"symfony/event-dispatcher": "^4.2|^5.0",
|
||||
"symfony/http-kernel": "^4.2|^5.0",
|
||||
"symfony/polyfill-mbstring": "^1.12"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-curl": "*",
|
||||
"ext-gmp": "*",
|
||||
"bjeavons/zxcvbn-php": "^1.0",
|
||||
"blackfire/php-sdk": "^1.14",
|
||||
"friendsofphp/php-cs-fixer": "^2.16",
|
||||
"infection/infection": "^0.15|^0.16|^0.17|^0.18|^0.19|^0.20",
|
||||
"matthiasnoback/symfony-config-test": "^3.1|^4.0",
|
||||
"nyholm/psr7": "^1.3",
|
||||
"php-coveralls/php-coveralls": "^2.0",
|
||||
"php-http/mock-client": "^1.0",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"phpstan/phpstan-deprecation-rules": "^0.12",
|
||||
"phpstan/phpstan-phpunit": "^0.12",
|
||||
"phpstan/phpstan-strict-rules": "^0.12",
|
||||
"phpunit/phpunit": "^8.0|^9.0",
|
||||
"symfony/browser-kit": "^4.2|^5.0",
|
||||
"symfony/finder": "^4.2|^5.0",
|
||||
"symfony/framework-bundle": "^4.2|^5.0",
|
||||
"symfony/http-client": "^5.2",
|
||||
"symfony/phpunit-bridge": "^4.2|^5.0",
|
||||
"symfony/serializer": "^4.2|^5.0",
|
||||
"symfony/var-dumper": "^4.2|^5.0"
|
||||
},
|
||||
"replace": {
|
||||
"web-token/jwt-easy": "self.version",
|
||||
"web-token/jwt-core": "self.version",
|
||||
"web-token/jwt-checker": "self.version",
|
||||
"web-token/jwt-signature": "self.version",
|
||||
"web-token/jwt-encryption": "self.version",
|
||||
"web-token/jwt-key-mgmt": "self.version",
|
||||
"web-token/jwt-console": "self.version",
|
||||
"web-token/jwt-nested-token": "self.version",
|
||||
"web-token/jwt-bundle": "self.version",
|
||||
"web-token/jwt-encryption-algorithm-aescbc": "self.version",
|
||||
"web-token/jwt-encryption-algorithm-aesgcm": "self.version",
|
||||
"web-token/jwt-encryption-algorithm-aesgcmkw": "self.version",
|
||||
"web-token/jwt-encryption-algorithm-aeskw": "self.version",
|
||||
"web-token/jwt-encryption-algorithm-dir": "self.version",
|
||||
"web-token/jwt-encryption-algorithm-ecdh-es": "self.version",
|
||||
"web-token/jwt-encryption-algorithm-pbes2": "self.version",
|
||||
"web-token/jwt-encryption-algorithm-rsa": "self.version",
|
||||
"web-token/jwt-signature-algorithm-ecdsa": "self.version",
|
||||
"web-token/jwt-signature-algorithm-eddsa": "self.version",
|
||||
"web-token/jwt-signature-algorithm-none": "self.version",
|
||||
"web-token/jwt-signature-algorithm-hmac": "self.version",
|
||||
"web-token/jwt-signature-algorithm-rsa": "self.version",
|
||||
"web-token/jwt-util-ecc": "self.version",
|
||||
"web-token/jwt-signature-algorithm-experimental": "self.version",
|
||||
"web-token/jwt-encryption-algorithm-experimental": "self.version",
|
||||
"web-token/signature-pack": "self.version",
|
||||
"web-token/encryption-pack": "self.version"
|
||||
|
||||
},
|
||||
"suggest":{
|
||||
"ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
|
||||
"bjeavons/zxcvbn-php": "Adds key quality check for oct keys.",
|
||||
"php-http/httplug": "To enable JKU/X5U support.",
|
||||
"php-http/httplug-bundle": "To enable JKU/X5U support.",
|
||||
"php-http/message-factory": "To enable JKU/X5U support.",
|
||||
"symfony/serializer": "Use the Symfony serializer to serialize/unserialize JWS and JWE tokens.",
|
||||
"symfony/var-dumper": "Used to show data on the debug toolbar."
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"v1.0": "1.0.x-dev",
|
||||
"v1.1": "1.1.x-dev",
|
||||
"v1.2": "1.2.x-dev",
|
||||
"v1.3": "1.3.x-dev",
|
||||
"v2.0": "2.0.x-dev",
|
||||
"v2.1": "2.1.x-dev"
|
||||
}
|
||||
},
|
||||
"conflict": {
|
||||
"spomky-labs/jose": "*"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"source": {
|
||||
"directories": [
|
||||
"src/Component",
|
||||
"src/Easy",
|
||||
"src/Ecc",
|
||||
"src/EncryptionAlgorithm",
|
||||
"src/SignatureAlgorithm"
|
||||
],
|
||||
"excludes": [
|
||||
"src/**/*/Tests"
|
||||
]
|
||||
},
|
||||
"timeout": 3,
|
||||
"logs": {
|
||||
"text": "infection.txt"
|
||||
},
|
||||
"mutators": {
|
||||
"@default": true,
|
||||
"@function_signature": true
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
parameters:
|
||||
level: 7
|
||||
paths:
|
||||
- src
|
||||
checkMissingIterableValueType: false
|
||||
checkGenericClassInNonGenericObjectType: false
|
||||
treatPhpDocTypesAsCertain: false
|
||||
ignoreErrors:
|
||||
- '#Strict comparison using === between false and .* will always evaluate to false\.*#'
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::children\(\)#'
|
||||
- '#Parameter \#1 \$value of class FG\\ASN1\\Universal\\Integer constructor expects int\, string given\.#'
|
||||
- '#Variable static method call on Jose\\Component\\Core\\Util\\Hash\.#'
|
||||
- '#Return type \(void\) of method Jose\\Bundle\\JoseFramework\\Routing\\JWKSetLoader::getResolver\(\) should be compatible with return type \(Symfony\\Component\\Config\\Loader\\LoaderResolverInterface\) of method Symfony\\Component\\Config\\Loader\\LoaderInterface::getResolver\(\)#'
|
||||
- '#Method Jose\\Bundle\\JoseFramework\\Routing\\JWKSetLoader::(supports|load)\(\) has parameter \$type with no typehint specified\.#'
|
||||
- '#.* should be contravariant with .*#'
|
||||
- '#Call to deprecated function .*#'
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||
- vendor/phpstan/phpstan-phpunit/extension.neon
|
||||
- vendor/phpstan/phpstan-phpunit/rules.neon
|
||||
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
|
||||
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
|
||||
@@ -0,0 +1,4 @@
|
||||
# Contributing
|
||||
|
||||
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
|
||||
Please do not submit any Pull Requests here. It will be automatically closed.
|
||||
@@ -0,0 +1 @@
|
||||
patreon: FlorentMorselli
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Please do not submit any Pull Requests here. It will be automatically closed.
|
||||
|
||||
You should submit it here: https://github.com/web-token/jwt-framework/pulls
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* This class is a header parameter checker.
|
||||
* When the "alg" header parameter is present, it will check if the value is within the allowed ones.
|
||||
*/
|
||||
final class AlgorithmChecker implements HeaderChecker
|
||||
{
|
||||
private const HEADER_NAME = 'alg';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $protectedHeader = false;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $supportedAlgorithms;
|
||||
|
||||
/**
|
||||
* @param string[] $supportedAlgorithms
|
||||
*/
|
||||
public function __construct(array $supportedAlgorithms, bool $protectedHeader = false)
|
||||
{
|
||||
$this->supportedAlgorithms = $supportedAlgorithms;
|
||||
$this->protectedHeader = $protectedHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function checkHeader($value): void
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new InvalidHeaderException('"alg" must be a string.', self::HEADER_NAME, $value);
|
||||
}
|
||||
if (!in_array($value, $this->supportedAlgorithms, true)) {
|
||||
throw new InvalidHeaderException('Unsupported algorithm.', self::HEADER_NAME, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function supportedHeader(): string
|
||||
{
|
||||
return self::HEADER_NAME;
|
||||
}
|
||||
|
||||
public function protectedHeaderOnly(): bool
|
||||
{
|
||||
return $this->protectedHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* This class is a header parameter and claim checker.
|
||||
* When the "aud" header parameter or claim is present, it will check if the value is within the allowed ones.
|
||||
*/
|
||||
final class AudienceChecker implements ClaimChecker, HeaderChecker
|
||||
{
|
||||
private const CLAIM_NAME = 'aud';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $protectedHeader = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $audience;
|
||||
|
||||
public function __construct(string $audience, bool $protectedHeader = false)
|
||||
{
|
||||
$this->audience = $audience;
|
||||
$this->protectedHeader = $protectedHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function checkClaim($value): void
|
||||
{
|
||||
$this->checkValue($value, InvalidClaimException::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function checkHeader($value): void
|
||||
{
|
||||
$this->checkValue($value, InvalidHeaderException::class);
|
||||
}
|
||||
|
||||
public function supportedClaim(): string
|
||||
{
|
||||
return self::CLAIM_NAME;
|
||||
}
|
||||
|
||||
public function supportedHeader(): string
|
||||
{
|
||||
return self::CLAIM_NAME;
|
||||
}
|
||||
|
||||
public function protectedHeaderOnly(): bool
|
||||
{
|
||||
return $this->protectedHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidClaimException if the claim is invalid
|
||||
* @throws InvalidHeaderException if the header is invalid
|
||||
*/
|
||||
private function checkValue($value, string $class): void
|
||||
{
|
||||
if (is_string($value) && $value !== $this->audience) {
|
||||
throw new $class('Bad audience.', self::CLAIM_NAME, $value);
|
||||
}
|
||||
if (is_array($value) && !in_array($this->audience, $value, true)) {
|
||||
throw new $class('Bad audience.', self::CLAIM_NAME, $value);
|
||||
}
|
||||
if (!is_array($value) && !is_string($value)) {
|
||||
throw new $class('Bad audience.', self::CLAIM_NAME, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
interface ClaimChecker
|
||||
{
|
||||
/**
|
||||
* When the token has the applicable claim, the value is checked.
|
||||
* If for some reason the value is not valid, an InvalidClaimException must be thrown.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidClaimException if the claim is invalid
|
||||
*/
|
||||
public function checkClaim($value): void;
|
||||
|
||||
/**
|
||||
* The method returns the claim to be checked.
|
||||
*/
|
||||
public function supportedClaim(): string;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* This manager handles as many claim checkers as needed.
|
||||
*/
|
||||
class ClaimCheckerManager
|
||||
{
|
||||
/**
|
||||
* @var ClaimChecker[]
|
||||
*/
|
||||
private $checkers = [];
|
||||
|
||||
/**
|
||||
* @param ClaimChecker[] $checkers
|
||||
*/
|
||||
public function __construct(array $checkers)
|
||||
{
|
||||
foreach ($checkers as $checker) {
|
||||
$this->add($checker);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns all checkers handled by this manager.
|
||||
*
|
||||
* @return ClaimChecker[]
|
||||
*/
|
||||
public function getCheckers(): array
|
||||
{
|
||||
return $this->checkers;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks all the claims passed as argument.
|
||||
* All claims are checked against the claim checkers.
|
||||
* If one fails, the InvalidClaimException is thrown.
|
||||
*
|
||||
* This method returns an array with all checked claims.
|
||||
* It is up to the implementor to decide use the claims that have not been checked.
|
||||
*
|
||||
* @param string[] $mandatoryClaims
|
||||
*
|
||||
* @throws InvalidClaimException
|
||||
* @throws MissingMandatoryClaimException
|
||||
*/
|
||||
public function check(array $claims, array $mandatoryClaims = []): array
|
||||
{
|
||||
$this->checkMandatoryClaims($mandatoryClaims, $claims);
|
||||
$checkedClaims = [];
|
||||
foreach ($this->checkers as $claim => $checker) {
|
||||
if (array_key_exists($claim, $claims)) {
|
||||
$checker->checkClaim($claims[$claim]);
|
||||
$checkedClaims[$claim] = $claims[$claim];
|
||||
}
|
||||
}
|
||||
|
||||
return $checkedClaims;
|
||||
}
|
||||
|
||||
private function add(ClaimChecker $checker): void
|
||||
{
|
||||
$claim = $checker->supportedClaim();
|
||||
$this->checkers[$claim] = $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $mandatoryClaims
|
||||
*
|
||||
* @throws MissingMandatoryClaimException
|
||||
*/
|
||||
private function checkMandatoryClaims(array $mandatoryClaims, array $claims): void
|
||||
{
|
||||
if (0 === count($mandatoryClaims)) {
|
||||
return;
|
||||
}
|
||||
$diff = array_keys(array_diff_key(array_flip($mandatoryClaims), $claims));
|
||||
if (0 !== count($diff)) {
|
||||
throw new MissingMandatoryClaimException(sprintf('The following claims are mandatory: %s.', implode(', ', $diff)), $diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class ClaimCheckerManagerFactory
|
||||
{
|
||||
/**
|
||||
* @var ClaimChecker[]
|
||||
*/
|
||||
private $checkers = [];
|
||||
|
||||
/**
|
||||
* This method creates a Claim Checker Manager and populate it with the claim checkers found based on the alias.
|
||||
* If the alias is not supported, an InvalidArgumentException is thrown.
|
||||
*
|
||||
* @param string[] $aliases
|
||||
*/
|
||||
public function create(array $aliases): ClaimCheckerManager
|
||||
{
|
||||
$checkers = [];
|
||||
foreach ($aliases as $alias) {
|
||||
if (!isset($this->checkers[$alias])) {
|
||||
throw new InvalidArgumentException(sprintf('The claim checker with the alias "%s" is not supported.', $alias));
|
||||
}
|
||||
$checkers[] = $this->checkers[$alias];
|
||||
}
|
||||
|
||||
return new ClaimCheckerManager($checkers);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method adds a claim checker to this factory.
|
||||
*/
|
||||
public function add(string $alias, ClaimChecker $checker): void
|
||||
{
|
||||
$this->checkers[$alias] = $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all claim checker aliases supported by this factory.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function aliases(): array
|
||||
{
|
||||
return array_keys($this->checkers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all claim checkers supported by this factory.
|
||||
*
|
||||
* @return ClaimChecker[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->checkers;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Exceptions thrown by this component.
|
||||
*/
|
||||
interface ClaimExceptionInterface extends Throwable
|
||||
{
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use function is_float;
|
||||
use function is_int;
|
||||
|
||||
/**
|
||||
* This class is a claim checker.
|
||||
* When the "exp" is present, it will compare the value with the current timestamp.
|
||||
*/
|
||||
final class ExpirationTimeChecker implements ClaimChecker, HeaderChecker
|
||||
{
|
||||
private const NAME = 'exp';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $allowedTimeDrift;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $protectedHeaderOnly;
|
||||
|
||||
public function __construct(int $allowedTimeDrift = 0, bool $protectedHeaderOnly = false)
|
||||
{
|
||||
$this->allowedTimeDrift = $allowedTimeDrift;
|
||||
$this->protectedHeaderOnly = $protectedHeaderOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws InvalidClaimException if the claim "exp" is not valid
|
||||
*/
|
||||
public function checkClaim($value): void
|
||||
{
|
||||
if (!is_float($value) && !is_int($value)) {
|
||||
throw new InvalidClaimException('"exp" must be an integer.', self::NAME, $value);
|
||||
}
|
||||
if (time() > $value + $this->allowedTimeDrift) {
|
||||
throw new InvalidClaimException('The token expired.', self::NAME, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function supportedClaim(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidHeaderException if the claim "exp" is not valid
|
||||
*/
|
||||
public function checkHeader($value): void
|
||||
{
|
||||
if (!is_float($value) && !is_int($value)) {
|
||||
throw new InvalidHeaderException('"exp" must be an integer.', self::NAME, $value);
|
||||
}
|
||||
if (time() > $value + $this->allowedTimeDrift) {
|
||||
throw new InvalidHeaderException('The token expired.', self::NAME, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function supportedHeader(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function protectedHeaderOnly(): bool
|
||||
{
|
||||
return $this->protectedHeaderOnly;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
interface HeaderChecker
|
||||
{
|
||||
/**
|
||||
* This method is called when the header parameter is present.
|
||||
* If for some reason the value is not valid, an InvalidHeaderException must be thrown.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidHeaderException if the header parameter is invalid
|
||||
*/
|
||||
public function checkHeader($value): void;
|
||||
|
||||
/**
|
||||
* The method returns the header parameter to be checked.
|
||||
*/
|
||||
public function supportedHeader(): string;
|
||||
|
||||
/**
|
||||
* When true, the header parameter to be checked MUST be set in the protected header of the token.
|
||||
*/
|
||||
public function protectedHeaderOnly(): bool;
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use Jose\Component\Core\JWT;
|
||||
|
||||
class HeaderCheckerManager
|
||||
{
|
||||
/**
|
||||
* @var HeaderChecker[]
|
||||
*/
|
||||
private $checkers = [];
|
||||
|
||||
/**
|
||||
* @var TokenTypeSupport[]
|
||||
*/
|
||||
private $tokenTypes = [];
|
||||
|
||||
/**
|
||||
* HeaderCheckerManager constructor.
|
||||
*
|
||||
* @param HeaderChecker[] $checkers
|
||||
* @param TokenTypeSupport[] $tokenTypes
|
||||
*/
|
||||
public function __construct(array $checkers, array $tokenTypes)
|
||||
{
|
||||
foreach ($checkers as $checker) {
|
||||
$this->add($checker);
|
||||
}
|
||||
foreach ($tokenTypes as $tokenType) {
|
||||
$this->addTokenTypeSupport($tokenType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns all checkers handled by this manager.
|
||||
*
|
||||
* @return HeaderChecker[]
|
||||
*/
|
||||
public function getCheckers(): array
|
||||
{
|
||||
return $this->checkers;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks all the header parameters passed as argument.
|
||||
* All header parameters are checked against the header parameter checkers.
|
||||
* If one fails, the InvalidHeaderException is thrown.
|
||||
*
|
||||
* @param string[] $mandatoryHeaderParameters
|
||||
*
|
||||
* @throws InvalidArgumentException if the token format is not valid
|
||||
*/
|
||||
public function check(JWT $jwt, int $index, array $mandatoryHeaderParameters = []): void
|
||||
{
|
||||
foreach ($this->tokenTypes as $tokenType) {
|
||||
if ($tokenType->supports($jwt)) {
|
||||
$protected = [];
|
||||
$unprotected = [];
|
||||
$tokenType->retrieveTokenHeaders($jwt, $index, $protected, $unprotected);
|
||||
$this->checkDuplicatedHeaderParameters($protected, $unprotected);
|
||||
$this->checkMandatoryHeaderParameters($mandatoryHeaderParameters, $protected, $unprotected);
|
||||
$this->checkHeaders($protected, $unprotected);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unsupported token type.');
|
||||
}
|
||||
|
||||
private function addTokenTypeSupport(TokenTypeSupport $tokenType): void
|
||||
{
|
||||
$this->tokenTypes[] = $tokenType;
|
||||
}
|
||||
|
||||
private function add(HeaderChecker $checker): void
|
||||
{
|
||||
$header = $checker->supportedHeader();
|
||||
$this->checkers[$header] = $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the header contains duplicated entries
|
||||
*/
|
||||
private function checkDuplicatedHeaderParameters(array $header1, array $header2): void
|
||||
{
|
||||
$inter = array_intersect_key($header1, $header2);
|
||||
if (0 !== count($inter)) {
|
||||
throw new InvalidArgumentException(sprintf('The header contains duplicated entries: %s.', implode(', ', array_keys($inter))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $mandatoryHeaderParameters
|
||||
*
|
||||
* @throws MissingMandatoryHeaderParameterException if a mandatory header parameter is missing
|
||||
*/
|
||||
private function checkMandatoryHeaderParameters(array $mandatoryHeaderParameters, array $protected, array $unprotected): void
|
||||
{
|
||||
if (0 === count($mandatoryHeaderParameters)) {
|
||||
return;
|
||||
}
|
||||
$diff = array_keys(array_diff_key(array_flip($mandatoryHeaderParameters), array_merge($protected, $unprotected)));
|
||||
if (0 !== count($diff)) {
|
||||
throw new MissingMandatoryHeaderParameterException(sprintf('The following header parameters are mandatory: %s.', implode(', ', $diff)), $diff);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidHeaderException if a protected header parameter is not in the protected header
|
||||
*/
|
||||
private function checkHeaders(array $protected, array $header): void
|
||||
{
|
||||
$checkedHeaderParameters = [];
|
||||
foreach ($this->checkers as $headerParameter => $checker) {
|
||||
if ($checker->protectedHeaderOnly()) {
|
||||
if (array_key_exists($headerParameter, $protected)) {
|
||||
$checker->checkHeader($protected[$headerParameter]);
|
||||
$checkedHeaderParameters[] = $headerParameter;
|
||||
} elseif (array_key_exists($headerParameter, $header)) {
|
||||
throw new InvalidHeaderException(sprintf('The header parameter "%s" must be protected.', $headerParameter), $headerParameter, $header[$headerParameter]);
|
||||
}
|
||||
} else {
|
||||
if (array_key_exists($headerParameter, $protected)) {
|
||||
$checker->checkHeader($protected[$headerParameter]);
|
||||
$checkedHeaderParameters[] = $headerParameter;
|
||||
} elseif (array_key_exists($headerParameter, $header)) {
|
||||
$checker->checkHeader($header[$headerParameter]);
|
||||
$checkedHeaderParameters[] = $headerParameter;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->checkCriticalHeader($protected, $header, $checkedHeaderParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidHeaderException if the "crit" parameter is not valid or if a critical header parameter cannot be verified
|
||||
*/
|
||||
private function checkCriticalHeader(array $protected, array $header, array $checkedHeaderParameters): void
|
||||
{
|
||||
if (array_key_exists('crit', $protected)) {
|
||||
if (!is_array($protected['crit'])) {
|
||||
throw new InvalidHeaderException('The header "crit" must be a list of header parameters.', 'crit', $protected['crit']);
|
||||
}
|
||||
$diff = array_diff($protected['crit'], $checkedHeaderParameters);
|
||||
if (0 !== count($diff)) {
|
||||
throw new InvalidHeaderException(sprintf('One or more header parameters are marked as critical, but they are missing or have not been checked: %s.', implode(', ', array_values($diff))), 'crit', $protected['crit']);
|
||||
}
|
||||
} elseif (array_key_exists('crit', $header)) {
|
||||
throw new InvalidHeaderException('The header parameter "crit" must be protected.', 'crit', $header['crit']);
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class HeaderCheckerManagerFactory
|
||||
{
|
||||
/**
|
||||
* @var HeaderChecker[]
|
||||
*/
|
||||
private $checkers = [];
|
||||
|
||||
/**
|
||||
* @var TokenTypeSupport[]
|
||||
*/
|
||||
private $tokenTypes = [];
|
||||
|
||||
/**
|
||||
* This method creates a Header Checker Manager and populate it with the header parameter checkers found based on the alias.
|
||||
* If the alias is not supported, an InvalidArgumentException is thrown.
|
||||
*
|
||||
* @param string[] $aliases
|
||||
*
|
||||
* @throws InvalidArgumentException if an alias is not supported
|
||||
*/
|
||||
public function create(array $aliases): HeaderCheckerManager
|
||||
{
|
||||
$checkers = [];
|
||||
foreach ($aliases as $alias) {
|
||||
if (!isset($this->checkers[$alias])) {
|
||||
throw new InvalidArgumentException(sprintf('The header checker with the alias "%s" is not supported.', $alias));
|
||||
}
|
||||
$checkers[] = $this->checkers[$alias];
|
||||
}
|
||||
|
||||
return new HeaderCheckerManager($checkers, $this->tokenTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method adds a header parameter checker to this factory.
|
||||
* The checker is uniquely identified by an alias. This allows the same header parameter checker to be added twice (or more)
|
||||
* using several configuration options.
|
||||
*/
|
||||
public function add(string $alias, HeaderChecker $checker): void
|
||||
{
|
||||
$this->checkers[$alias] = $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method adds a token type support to this factory.
|
||||
*/
|
||||
public function addTokenTypeSupport(TokenTypeSupport $tokenType): void
|
||||
{
|
||||
$this->tokenTypes[] = $tokenType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all header parameter checker aliases supported by this factory.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function aliases(): array
|
||||
{
|
||||
return array_keys($this->checkers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all header parameter checkers supported by this factory.
|
||||
*
|
||||
* @return HeaderChecker[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->checkers;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* This exception is thrown by claim checkers when a claim check failed.
|
||||
*/
|
||||
class InvalidClaimException extends Exception implements ClaimExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $claim;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct(string $message, string $claim, $value)
|
||||
{
|
||||
parent::__construct($message);
|
||||
|
||||
$this->claim = $claim;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the claim that caused the exception.
|
||||
*/
|
||||
public function getClaim(): string
|
||||
{
|
||||
return $this->claim;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the claim value that caused the exception.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* This exception is thrown by header parameter checkers when a header parameter check failed.
|
||||
*/
|
||||
class InvalidHeaderException extends Exception
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $header;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct(string $message, string $header, $value)
|
||||
{
|
||||
parent::__construct($message);
|
||||
|
||||
$this->header = $header;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header parameter that caused the exception.
|
||||
*/
|
||||
public function getHeader(): string
|
||||
{
|
||||
return $this->header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header parameter value that caused the exception.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use function is_float;
|
||||
use function is_int;
|
||||
|
||||
/**
|
||||
* This class is a claim checker.
|
||||
* When the "iat" is present, it will compare the value with the current timestamp.
|
||||
*/
|
||||
final class IssuedAtChecker implements ClaimChecker, HeaderChecker
|
||||
{
|
||||
private const NAME = 'iat';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $allowedTimeDrift;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $protectedHeaderOnly;
|
||||
|
||||
public function __construct(int $allowedTimeDrift = 0, bool $protectedHeaderOnly = false)
|
||||
{
|
||||
$this->allowedTimeDrift = $allowedTimeDrift;
|
||||
$this->protectedHeaderOnly = $protectedHeaderOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws InvalidClaimException if the claim is invalid
|
||||
*/
|
||||
public function checkClaim($value): void
|
||||
{
|
||||
if (!is_float($value) && !is_int($value)) {
|
||||
throw new InvalidClaimException('"iat" must be an integer.', self::NAME, $value);
|
||||
}
|
||||
if (time() < $value - $this->allowedTimeDrift) {
|
||||
throw new InvalidClaimException('The JWT is issued in the future.', self::NAME, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function supportedClaim(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidHeaderException if the header parameter is invalid
|
||||
*/
|
||||
public function checkHeader($value): void
|
||||
{
|
||||
if (!is_float($value) && !is_int($value)) {
|
||||
throw new InvalidHeaderException('The header "iat" must be an integer.', self::NAME, $value);
|
||||
}
|
||||
if (time() < $value - $this->allowedTimeDrift) {
|
||||
throw new InvalidHeaderException('The JWT is issued in the future.', self::NAME, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function supportedHeader(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function protectedHeaderOnly(): bool
|
||||
{
|
||||
return $this->protectedHeaderOnly;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* This class is a header parameter and claim checker.
|
||||
* When the "iss" header parameter or claim is present, it will check if the value is within the allowed ones.
|
||||
*/
|
||||
final class IssuerChecker implements ClaimChecker, HeaderChecker
|
||||
{
|
||||
private const CLAIM_NAME = 'iss';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $protectedHeader = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $issuers;
|
||||
|
||||
public function __construct(array $issuer, bool $protectedHeader = false)
|
||||
{
|
||||
$this->issuers = $issuer;
|
||||
$this->protectedHeader = $protectedHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidClaimException if the claim is invalid
|
||||
*/
|
||||
public function checkClaim($value): void
|
||||
{
|
||||
$this->checkValue($value, InvalidClaimException::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidHeaderException if the header parameter is invalid
|
||||
*/
|
||||
public function checkHeader($value): void
|
||||
{
|
||||
$this->checkValue($value, InvalidHeaderException::class);
|
||||
}
|
||||
|
||||
public function supportedClaim(): string
|
||||
{
|
||||
return self::CLAIM_NAME;
|
||||
}
|
||||
|
||||
public function supportedHeader(): string
|
||||
{
|
||||
return self::CLAIM_NAME;
|
||||
}
|
||||
|
||||
public function protectedHeaderOnly(): bool
|
||||
{
|
||||
return $this->protectedHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidClaimException if the claim is invalid
|
||||
* @throws InvalidHeaderException if the header parameter is invalid
|
||||
*/
|
||||
private function checkValue($value, string $class): void
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new $class('Invalid value.', self::CLAIM_NAME, $value);
|
||||
}
|
||||
if (!in_array($value, $this->issuers, true)) {
|
||||
throw new $class('Unknown issuer.', self::CLAIM_NAME, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2019 Spomky-Labs
|
||||
|
||||
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, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use Exception;
|
||||
|
||||
class MissingMandatoryClaimException extends Exception implements ClaimExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $claims;
|
||||
|
||||
/**
|
||||
* MissingMandatoryClaimException constructor.
|
||||
*
|
||||
* @param string[] $claims
|
||||
*/
|
||||
public function __construct(string $message, array $claims)
|
||||
{
|
||||
parent::__construct($message);
|
||||
|
||||
$this->claims = $claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getClaims(): array
|
||||
{
|
||||
return $this->claims;
|
||||
}
|
||||
}
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use Exception;
|
||||
|
||||
class MissingMandatoryHeaderParameterException extends Exception
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $parameters;
|
||||
|
||||
/**
|
||||
* MissingMandatoryHeaderParameterException constructor.
|
||||
*
|
||||
* @param string[] $parameters
|
||||
*/
|
||||
public function __construct(string $message, array $parameters)
|
||||
{
|
||||
parent::__construct($message);
|
||||
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use function is_float;
|
||||
use function is_int;
|
||||
|
||||
/**
|
||||
* This class is a claim checker.
|
||||
* When the "nbf" is present, it will compare the value with the current timestamp.
|
||||
*/
|
||||
final class NotBeforeChecker implements ClaimChecker, HeaderChecker
|
||||
{
|
||||
private const NAME = 'nbf';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $allowedTimeDrift;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $protectedHeaderOnly;
|
||||
|
||||
public function __construct(int $allowedTimeDrift = 0, bool $protectedHeaderOnly = false)
|
||||
{
|
||||
$this->allowedTimeDrift = $allowedTimeDrift;
|
||||
$this->protectedHeaderOnly = $protectedHeaderOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws InvalidClaimException if the claim "nbf" is not an integer
|
||||
* @throws InvalidClaimException if the claim "nbf" restrict the use of the token
|
||||
*/
|
||||
public function checkClaim($value): void
|
||||
{
|
||||
if (!is_float($value) && !is_int($value)) {
|
||||
throw new InvalidClaimException('"nbf" must be an integer.', self::NAME, $value);
|
||||
}
|
||||
if (time() < $value - $this->allowedTimeDrift) {
|
||||
throw new InvalidClaimException('The JWT can not be used yet.', self::NAME, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function supportedClaim(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidHeaderException if the claim "nbf" is not an integer
|
||||
* @throws InvalidHeaderException if the claim "nbf" restrict the use of the token
|
||||
*/
|
||||
public function checkHeader($value): void
|
||||
{
|
||||
if (!is_float($value) && !is_int($value)) {
|
||||
throw new InvalidHeaderException('"nbf" must be an integer.', self::NAME, $value);
|
||||
}
|
||||
if (time() < $value - $this->allowedTimeDrift) {
|
||||
throw new InvalidHeaderException('The JWT can not be used yet.', self::NAME, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function supportedHeader(): string
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function protectedHeaderOnly(): bool
|
||||
{
|
||||
return $this->protectedHeaderOnly;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
PHP JWT Checker Component
|
||||
=========================
|
||||
|
||||
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
|
||||
|
||||
**Please do not submit any Pull Request here.**
|
||||
You should go to [the main repository](https://github.com/web-token/jwt-framework) instead.
|
||||
|
||||
# Documentation
|
||||
|
||||
The official documentation is available as https://web-token.spomky-labs.com/
|
||||
|
||||
# Licence
|
||||
|
||||
This software is release under [MIT licence](LICENSE).
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use Jose\Component\Core\JWT;
|
||||
|
||||
interface TokenTypeSupport
|
||||
{
|
||||
/**
|
||||
* This method will retrieve the protect and unprotected headers of the token for the given index.
|
||||
* The index is useful when the token is serialized using the Json General Serialization mode.
|
||||
* For example the JWE Json General Serialization Mode allows several recipients to be set.
|
||||
* The unprotected headers correspond to the share unprotected header and the selected recipient header.
|
||||
*/
|
||||
public function retrieveTokenHeaders(JWT $jwt, int $index, array &$protectedHeader, array &$unprotectedHeader): void;
|
||||
|
||||
/**
|
||||
* This method returns true if the token in argument is supported, otherwise false.
|
||||
*/
|
||||
public function supports(JWT $jwt): bool;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Checker;
|
||||
|
||||
use function is_bool;
|
||||
|
||||
/**
|
||||
* This class is a header parameter checker.
|
||||
* When the "b64" is present, it will check if the value is a boolean or not.
|
||||
*
|
||||
* The use of this checker will allow the use of token with unencoded payload.
|
||||
*/
|
||||
final class UnencodedPayloadChecker implements HeaderChecker
|
||||
{
|
||||
private const HEADER_NAME = 'b64';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws InvalidHeaderException if the header parameter "b64" is not a boolean
|
||||
*/
|
||||
public function checkHeader($value): void
|
||||
{
|
||||
if (!is_bool($value)) {
|
||||
throw new InvalidHeaderException('"b64" must be a boolean.', self::HEADER_NAME, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function supportedHeader(): string
|
||||
{
|
||||
return self::HEADER_NAME;
|
||||
}
|
||||
|
||||
public function protectedHeaderOnly(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "web-token/jwt-checker",
|
||||
"description": "Checker component of the JWT Framework.",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"],
|
||||
"homepage": "https://github.com/web-token",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Florent Morselli",
|
||||
"homepage": "https://github.com/Spomky"
|
||||
},{
|
||||
"name": "All contributors",
|
||||
"homepage": "https://github.com/web-token/jwt-checker/contributors"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jose\\Component\\Checker\\": ""
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"web-token/jwt-core": "^2.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Contributing
|
||||
|
||||
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
|
||||
Please do not submit any Pull Requests here. It will be automatically closed.
|
||||
@@ -0,0 +1 @@
|
||||
patreon: FlorentMorselli
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Please do not submit any Pull Requests here. It will be automatically closed.
|
||||
|
||||
You should submit it here: https://github.com/web-token/jwt-framework/pulls
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWK;
|
||||
use Jose\Component\Core\JWKSet;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class AddKeyIntoKeysetCommand extends ObjectOutputCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:add:key')
|
||||
->setDescription('Add a key into a key set.')
|
||||
->setHelp('This command adds a key at the end of a key set.')
|
||||
->addArgument('jwkset', InputArgument::REQUIRED, 'The JWKSet object')
|
||||
->addArgument('jwk', InputArgument::REQUIRED, 'The new JWK object')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$jwkset = $this->getKeyset($input);
|
||||
$jwk = $this->getKey($input);
|
||||
$jwkset = $jwkset->with($jwk);
|
||||
$this->prepareJsonOutput($input, $output, $jwkset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key set is invalid
|
||||
*/
|
||||
private function getKeyset(InputInterface $input): JWKSet
|
||||
{
|
||||
$jwkset = $input->getArgument('jwkset');
|
||||
if (!is_string($jwkset)) {
|
||||
throw new InvalidArgumentException('The argument must be a valid JWKSet.');
|
||||
}
|
||||
$json = JsonConverter::decode($jwkset);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('The argument must be a valid JWKSet.');
|
||||
}
|
||||
|
||||
return JWKSet::createFromKeyData($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key is invalid
|
||||
*/
|
||||
private function getKey(InputInterface $input): JWK
|
||||
{
|
||||
$jwk = $input->getArgument('jwk');
|
||||
if (!is_string($jwk)) {
|
||||
throw new InvalidArgumentException('The argument must be a valid JWK.');
|
||||
}
|
||||
$json = JsonConverter::decode($jwk);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('The argument must be a valid JWK.');
|
||||
}
|
||||
|
||||
return new JWK($json);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_string;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class EcKeyGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:generate:ec')
|
||||
->setDescription('Generate an EC key (JWK format)')
|
||||
->addArgument('curve', InputArgument::REQUIRED, 'Curve of the key.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the curve is invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$curve = $input->getArgument('curve');
|
||||
if (!is_string($curve)) {
|
||||
throw new InvalidArgumentException('Invalid curve');
|
||||
}
|
||||
$args = $this->getOptions($input);
|
||||
|
||||
$jwk = JWKFactory::createECKey($curve, $args);
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWKSet;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class EcKeysetGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:generate:ec')
|
||||
->setDescription('Generate an EC key set (JWKSet format)')
|
||||
->addArgument('quantity', InputArgument::REQUIRED, 'Quantity of keys in the key set.')
|
||||
->addArgument('curve', InputArgument::REQUIRED, 'Curve of the keys.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the quantity of keys is invalid
|
||||
* @throws InvalidArgumentException if the curve is invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$quantity = $input->getArgument('quantity');
|
||||
if (null === $quantity) {
|
||||
$quantity = 1;
|
||||
} elseif (is_array($quantity)) {
|
||||
$quantity = 1;
|
||||
} else {
|
||||
$quantity = (int) $quantity;
|
||||
}
|
||||
if ($quantity < 1) {
|
||||
throw new InvalidArgumentException('Invalid quantity');
|
||||
}
|
||||
$curve = $input->getArgument('curve');
|
||||
if (!is_string($curve)) {
|
||||
throw new InvalidArgumentException('Invalid curve');
|
||||
}
|
||||
|
||||
$keyset = new JWKSet([]);
|
||||
for ($i = 0; $i < $quantity; ++$i) {
|
||||
$args = $this->getOptions($input);
|
||||
$keyset = $keyset->with(JWKFactory::createECKey($curve, $args));
|
||||
}
|
||||
$this->prepareJsonOutput($input, $output, $keyset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use Base64Url\Base64Url;
|
||||
use InvalidArgumentException;
|
||||
use function is_bool;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
abstract class GeneratorCommand extends ObjectOutputCommand
|
||||
{
|
||||
public function isEnabled()
|
||||
{
|
||||
return class_exists(JWKFactory::class);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->addOption('use', 'u', InputOption::VALUE_OPTIONAL, 'Usage of the key. Must be either "sig" or "enc".')
|
||||
->addOption('alg', 'a', InputOption::VALUE_OPTIONAL, 'Algorithm for the key.')
|
||||
->addOption('random_id', null, InputOption::VALUE_NONE, 'If this option is set, a random key ID (kid) will be generated.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the option "random_id" is not a valid
|
||||
*/
|
||||
protected function getOptions(InputInterface $input): array
|
||||
{
|
||||
$args = [];
|
||||
$useRandomId = $input->getOption('random_id');
|
||||
if (!is_bool($useRandomId)) {
|
||||
throw new InvalidArgumentException('Invalid value for option "random_id"');
|
||||
}
|
||||
if ($useRandomId) {
|
||||
$args['kid'] = $this->generateKeyID();
|
||||
}
|
||||
foreach (['use', 'alg'] as $key) {
|
||||
$value = $input->getOption($key);
|
||||
if (null !== $value) {
|
||||
$args[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
private function generateKeyID(): string
|
||||
{
|
||||
return Base64Url::encode(random_bytes(32));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWK;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class GetThumbprintCommand extends ObjectOutputCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:thumbprint')
|
||||
->setDescription('Get the thumbprint of a JWK key.')
|
||||
->addArgument('jwk', InputArgument::REQUIRED, 'The JWK key.')
|
||||
->addOption('hash', null, InputOption::VALUE_OPTIONAL, 'The hashing algorithm.', 'sha256')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the JWK or the hashing function are invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$jwk = $input->getArgument('jwk');
|
||||
if (!is_string($jwk)) {
|
||||
throw new InvalidArgumentException('Invalid JWK');
|
||||
}
|
||||
$hash = $input->getOption('hash');
|
||||
if (!is_string($hash)) {
|
||||
throw new InvalidArgumentException('Invalid hash algorithm');
|
||||
}
|
||||
$json = JsonConverter::decode($jwk);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('Invalid input.');
|
||||
}
|
||||
$key = new JWK($json);
|
||||
$output->write($key->thumbprint($hash));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_string;
|
||||
use Jose\Component\KeyManagement\JKUFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class JKULoaderCommand extends ObjectOutputCommand
|
||||
{
|
||||
/**
|
||||
* @var JKUFactory
|
||||
*/
|
||||
private $jkuFactory;
|
||||
|
||||
public function __construct(JKUFactory $jkuFactory, ?string $name = null)
|
||||
{
|
||||
$this->jkuFactory = $jkuFactory;
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:load:jku')
|
||||
->setDescription('Loads a key set from an url.')
|
||||
->setHelp('This command will try to get a key set from an URL. The distant key set is a JWKSet.')
|
||||
->addArgument('url', InputArgument::REQUIRED, 'The URL')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the URL is invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$url = $input->getArgument('url');
|
||||
if (!is_string($url)) {
|
||||
throw new InvalidArgumentException('Invalid URL');
|
||||
}
|
||||
$result = $this->jkuFactory->loadFromUrl($url);
|
||||
$this->prepareJsonOutput($input, $output, $result);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWK;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Jose\Component\KeyManagement\Analyzer\KeyAnalyzerManager;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class KeyAnalyzerCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var KeyAnalyzerManager
|
||||
*/
|
||||
private $analyzerManager;
|
||||
|
||||
public function __construct(KeyAnalyzerManager $keysetAnalyzerManager, string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
$this->analyzerManager = $keysetAnalyzerManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:analyze')
|
||||
->setDescription('JWK quality analyzer.')
|
||||
->setHelp('This command will analyze a JWK object and find security issues.')
|
||||
->addArgument('jwk', InputArgument::REQUIRED, 'The JWK object')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$output->getFormatter()->setStyle('success', new OutputFormatterStyle('white', 'green'));
|
||||
$output->getFormatter()->setStyle('high', new OutputFormatterStyle('white', 'red', ['bold']));
|
||||
$output->getFormatter()->setStyle('medium', new OutputFormatterStyle('yellow'));
|
||||
$output->getFormatter()->setStyle('low', new OutputFormatterStyle('blue'));
|
||||
$jwk = $this->getKey($input);
|
||||
|
||||
$result = $this->analyzerManager->analyze($jwk);
|
||||
if (0 === $result->count()) {
|
||||
$output->writeln('<success>All good! No issue found.</success>');
|
||||
} else {
|
||||
foreach ($result->all() as $message) {
|
||||
$output->writeln('<'.$message->getSeverity().'>* '.$message->getMessage().'</'.$message->getSeverity().'>');
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key is invalid
|
||||
*/
|
||||
private function getKey(InputInterface $input): JWK
|
||||
{
|
||||
$jwk = $input->getArgument('jwk');
|
||||
if (!is_string($jwk)) {
|
||||
throw new InvalidArgumentException('Invalid JWK');
|
||||
}
|
||||
$json = JsonConverter::decode($jwk);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('Invalid JWK.');
|
||||
}
|
||||
|
||||
return new JWK($json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_string;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class KeyFileLoaderCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:load:key')
|
||||
->setDescription('Loads a key from a key file (JWK format)')
|
||||
->addArgument('file', InputArgument::REQUIRED, 'Filename of the key.')
|
||||
->addOption('secret', 's', InputOption::VALUE_OPTIONAL, 'Secret if the key is encrypted.', null)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the file is invalid
|
||||
* @throws InvalidArgumentException if the secret is invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$file = $input->getArgument('file');
|
||||
$password = $input->getOption('secret');
|
||||
if (!is_string($file)) {
|
||||
throw new InvalidArgumentException('Invalid file');
|
||||
}
|
||||
if (null !== $password && !is_string($password)) {
|
||||
throw new InvalidArgumentException('Invalid secret');
|
||||
}
|
||||
$args = $this->getOptions($input);
|
||||
|
||||
$jwk = JWKFactory::createFromKeyFile($file, $password, $args);
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWKSet;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Jose\Component\KeyManagement\Analyzer\KeyAnalyzerManager;
|
||||
use Jose\Component\KeyManagement\Analyzer\KeysetAnalyzerManager;
|
||||
use Jose\Component\KeyManagement\Analyzer\MessageBag;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class KeysetAnalyzerCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var KeysetAnalyzerManager
|
||||
*/
|
||||
private $keysetAnalyzerManager;
|
||||
|
||||
/**
|
||||
* @var KeyAnalyzerManager
|
||||
*/
|
||||
private $keyAnalyzerManager;
|
||||
|
||||
public function __construct(KeysetAnalyzerManager $keysetAnalyzerManager, KeyAnalyzerManager $keyAnalyzerManager, string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
$this->keysetAnalyzerManager = $keysetAnalyzerManager;
|
||||
$this->keyAnalyzerManager = $keyAnalyzerManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:analyze')
|
||||
->setDescription('JWKSet quality analyzer.')
|
||||
->setHelp('This command will analyze a JWKSet object and find security issues.')
|
||||
->addArgument('jwkset', InputArgument::REQUIRED, 'The JWKSet object')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$output->getFormatter()->setStyle('success', new OutputFormatterStyle('white', 'green'));
|
||||
$output->getFormatter()->setStyle('high', new OutputFormatterStyle('white', 'red', ['bold']));
|
||||
$output->getFormatter()->setStyle('medium', new OutputFormatterStyle('yellow'));
|
||||
$output->getFormatter()->setStyle('low', new OutputFormatterStyle('blue'));
|
||||
|
||||
$jwkset = $this->getKeyset($input);
|
||||
|
||||
$messages = $this->keysetAnalyzerManager->analyze($jwkset);
|
||||
$this->showMessages($messages, $output);
|
||||
foreach ($jwkset as $kid => $jwk) {
|
||||
$output->writeln(sprintf('Analysing key with index/kid "%s"', $kid));
|
||||
$messages = $this->keyAnalyzerManager->analyze($jwk);
|
||||
$this->showMessages($messages, $output);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function showMessages(MessageBag $messages, OutputInterface $output): void
|
||||
{
|
||||
if (0 === $messages->count()) {
|
||||
$output->writeln(' <success>All good! No issue found.</success>');
|
||||
} else {
|
||||
foreach ($messages->all() as $message) {
|
||||
$output->writeln(' <'.$message->getSeverity().'>* '.$message->getMessage().'</'.$message->getSeverity().'>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the JWKSet is invalid
|
||||
*/
|
||||
private function getKeyset(InputInterface $input): JWKSet
|
||||
{
|
||||
$jwkset = $input->getArgument('jwkset');
|
||||
if (!is_string($jwkset)) {
|
||||
throw new InvalidArgumentException('Invalid JWKSet');
|
||||
}
|
||||
$json = JsonConverter::decode($jwkset);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('Invalid JWKSet');
|
||||
}
|
||||
|
||||
return JWKSet::createFromKeyData($json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2019 Spomky-Labs
|
||||
|
||||
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, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use Jose\Component\Core\JWKSet;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class MergeKeysetCommand extends ObjectOutputCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:merge')
|
||||
->setDescription('Merge several key sets into one.')
|
||||
->setHelp('This command merges several key sets into one. It is very useful when you generate e.g. RSA, EC and OKP keys and you want only one key set to rule them all.')
|
||||
->addArgument('jwksets', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'The JWKSet objects')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the JWKSet is invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
/** @var string[] $keySets */
|
||||
$keySets = $input->getArgument('jwksets');
|
||||
$newJwkset = new JWKSet([]);
|
||||
foreach ($keySets as $keySet) {
|
||||
$json = JsonConverter::decode($keySet);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('The argument must be a valid JWKSet.');
|
||||
}
|
||||
$jwkset = JWKSet::createFromKeyData($json);
|
||||
foreach ($jwkset->all() as $jwk) {
|
||||
$newJwkset = $newJwkset->with($jwk);
|
||||
}
|
||||
}
|
||||
$this->prepareJsonOutput($input, $output, $newJwkset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class NoneKeyGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:generate:none')
|
||||
->setDescription('Generate a none key (JWK format). This key type is only supposed to be used with the "none" algorithm.')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$args = $this->getOptions($input);
|
||||
|
||||
$jwk = JWKFactory::createNoneKey($args);
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use JsonSerializable;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
abstract class ObjectOutputCommand extends Command
|
||||
{
|
||||
protected function prepareJsonOutput(InputInterface $input, OutputInterface $output, JsonSerializable $json): void
|
||||
{
|
||||
$data = JsonConverter::encode($json);
|
||||
$output->write($data);
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class OctKeyGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:generate:oct')
|
||||
->setDescription('Generate an octet key (JWK format)')
|
||||
->addArgument('size', InputArgument::REQUIRED, 'Key size.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key size is not valid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$size = $input->getArgument('size');
|
||||
if (null === $size) {
|
||||
$size = 1;
|
||||
} elseif (is_array($size)) {
|
||||
$size = 1;
|
||||
} else {
|
||||
$size = (int) $size;
|
||||
}
|
||||
if ($size < 1) {
|
||||
throw new InvalidArgumentException('Invalid size');
|
||||
}
|
||||
$args = $this->getOptions($input);
|
||||
|
||||
$jwk = JWKFactory::createOctKey($size, $args);
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use Jose\Component\Core\JWKSet;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class OctKeysetGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:generate:oct')
|
||||
->setDescription('Generate a key set with octet keys (JWK format)')
|
||||
->addArgument('quantity', InputArgument::REQUIRED, 'Quantity of keys in the key set.')
|
||||
->addArgument('size', InputArgument::REQUIRED, 'Key size.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the quantity is not valid
|
||||
* @throws InvalidArgumentException if the key size is not valid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$quantity = $input->getArgument('quantity');
|
||||
if (null === $quantity) {
|
||||
$quantity = 1;
|
||||
} elseif (is_array($quantity)) {
|
||||
$quantity = 1;
|
||||
} else {
|
||||
$quantity = (int) $quantity;
|
||||
}
|
||||
|
||||
$size = $input->getArgument('size');
|
||||
if (null === $size) {
|
||||
$size = 1;
|
||||
} elseif (is_array($size)) {
|
||||
$size = 1;
|
||||
} else {
|
||||
$size = (int) $size;
|
||||
}
|
||||
if ($quantity < 1) {
|
||||
throw new InvalidArgumentException('Invalid quantity');
|
||||
}
|
||||
if ($size < 1) {
|
||||
throw new InvalidArgumentException('Invalid size');
|
||||
}
|
||||
|
||||
$keyset = new JWKSet([]);
|
||||
for ($i = 0; $i < $quantity; ++$i) {
|
||||
$args = $this->getOptions($input);
|
||||
$keyset = $keyset->with(JWKFactory::createOctKey($size, $args));
|
||||
}
|
||||
$this->prepareJsonOutput($input, $output, $keyset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_string;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class OkpKeyGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:generate:okp')
|
||||
->setDescription('Generate an Octet Key Pair key (JWK format)')
|
||||
->addArgument('curve', InputArgument::REQUIRED, 'Curve of the key.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the curve is not valid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$curve = $input->getArgument('curve');
|
||||
if (!is_string($curve)) {
|
||||
throw new InvalidArgumentException('Invalid curve');
|
||||
}
|
||||
$args = $this->getOptions($input);
|
||||
|
||||
$jwk = JWKFactory::createOKPKey($curve, $args);
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWKSet;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class OkpKeysetGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:generate:okp')
|
||||
->setDescription('Generate a key set with Octet Key Pairs keys (JWKSet format)')
|
||||
->addArgument('quantity', InputArgument::REQUIRED, 'Quantity of keys in the key set.')
|
||||
->addArgument('curve', InputArgument::REQUIRED, 'Curve of the keys.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the curve is not valid
|
||||
* @throws InvalidArgumentException if the quantity is not valid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$quantity = $input->getArgument('quantity');
|
||||
if (null === $quantity) {
|
||||
$quantity = 1;
|
||||
} elseif (is_array($quantity)) {
|
||||
$quantity = 1;
|
||||
} else {
|
||||
$quantity = (int) $quantity;
|
||||
}
|
||||
$curve = $input->getArgument('curve');
|
||||
if ($quantity < 1) {
|
||||
throw new InvalidArgumentException('Invalid quantity');
|
||||
}
|
||||
if (!is_string($curve)) {
|
||||
throw new InvalidArgumentException('Invalid curve');
|
||||
}
|
||||
|
||||
$keyset = new JWKSet([]);
|
||||
for ($i = 0; $i < $quantity; ++$i) {
|
||||
$args = $this->getOptions($input);
|
||||
$keyset = $keyset->with(JWKFactory::createOKPKey($curve, $args));
|
||||
}
|
||||
$this->prepareJsonOutput($input, $output, $keyset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWK;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Jose\Component\KeyManagement\KeyConverter\RSAKey;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class OptimizeRsaKeyCommand extends ObjectOutputCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:optimize')
|
||||
->setDescription('Optimize a RSA key by calculating additional primes (CRT).')
|
||||
->addArgument('jwk', InputArgument::REQUIRED, 'The RSA key.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key is not valid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$jwk = $input->getArgument('jwk');
|
||||
if (!is_string($jwk)) {
|
||||
throw new InvalidArgumentException('Invalid JWK');
|
||||
}
|
||||
$json = JsonConverter::decode($jwk);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('Invalid JWK');
|
||||
}
|
||||
$key = RSAKey::createFromJWK(new JWK($json));
|
||||
$key->optimize();
|
||||
$this->prepareJsonOutput($input, $output, $key->toJwk());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_string;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class P12CertificateLoaderCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:load:p12')
|
||||
->setDescription('Load a key from a P12 certificate file.')
|
||||
->addArgument('file', InputArgument::REQUIRED, 'Filename of the P12 certificate.')
|
||||
->addOption('secret', 's', InputOption::VALUE_OPTIONAL, 'Secret if the key is encrypted.', null)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the file is not valid
|
||||
* @throws InvalidArgumentException if the secret is not valid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$file = $input->getArgument('file');
|
||||
$password = $input->getOption('secret');
|
||||
if (!is_string($file)) {
|
||||
throw new InvalidArgumentException('Invalid file');
|
||||
}
|
||||
if (!is_string($password)) {
|
||||
throw new InvalidArgumentException('Invalid secret');
|
||||
}
|
||||
$args = $this->getOptions($input);
|
||||
$jwk = JWKFactory::createFromPKCS12CertificateFile($file, $password, $args);
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWK;
|
||||
use Jose\Component\Core\Util\ECKey;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Jose\Component\Core\Util\RSAKey;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class PemConverterCommand extends ObjectOutputCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:convert:pkcs1')
|
||||
->setDescription('Converts a RSA or EC key into PKCS#1 key.')
|
||||
->addArgument('jwk', InputArgument::REQUIRED, 'The key')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key is invalid
|
||||
* @throws InvalidArgumentException if the key type is not RSA or EC
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$jwk = $input->getArgument('jwk');
|
||||
if (!is_string($jwk)) {
|
||||
throw new InvalidArgumentException('Invalid JWK');
|
||||
}
|
||||
$json = JsonConverter::decode($jwk);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('Invalid JWK.');
|
||||
}
|
||||
$key = new JWK($json);
|
||||
|
||||
switch ($key->get('kty')) {
|
||||
case 'RSA':
|
||||
$pem = RSAKey::createFromJWK($key)->toPEM();
|
||||
|
||||
break;
|
||||
|
||||
case 'EC':
|
||||
$pem = ECKey::convertToPEM($key);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException('Not a RSA or EC key.');
|
||||
}
|
||||
$output->write($pem);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWK;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class PublicKeyCommand extends ObjectOutputCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:convert:public')
|
||||
->setDescription('Convert a private key into public key. Symmetric keys (shared keys) are not changed.')
|
||||
->setHelp('This command converts a private key into a public key.')
|
||||
->addArgument('jwk', InputArgument::REQUIRED, 'The JWK object')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$jwk = $this->getKey($input);
|
||||
$jwk = $jwk->toPublic();
|
||||
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key is invalid
|
||||
*/
|
||||
private function getKey(InputInterface $input): JWK
|
||||
{
|
||||
$jwk = $input->getArgument('jwk');
|
||||
if (!is_string($jwk)) {
|
||||
throw new InvalidArgumentException('Invalid JWK');
|
||||
}
|
||||
$json = JsonConverter::decode($jwk);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('Invalid JWK');
|
||||
}
|
||||
|
||||
return new JWK($json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWKSet;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class PublicKeysetCommand extends ObjectOutputCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:convert:public')
|
||||
->setDescription('Convert private keys in a key set into public keys. Symmetric keys (shared keys) are not changed.')
|
||||
->setHelp('This command converts private keys in a key set into public keys.')
|
||||
->addArgument('jwkset', InputArgument::REQUIRED, 'The JWKSet object')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$jwkset = $this->getKeyset($input);
|
||||
$newJwkset = new JWKSet([]);
|
||||
|
||||
foreach ($jwkset->all() as $jwk) {
|
||||
$newJwkset = $newJwkset->with($jwk->toPublic());
|
||||
}
|
||||
$this->prepareJsonOutput($input, $output, $newJwkset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the keyset is invalid
|
||||
*/
|
||||
private function getKeyset(InputInterface $input): JWKSet
|
||||
{
|
||||
$jwkset = $input->getArgument('jwkset');
|
||||
if (!is_string($jwkset)) {
|
||||
throw new InvalidArgumentException('Invalid JWKSet');
|
||||
}
|
||||
$json = JsonConverter::decode($jwkset);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('Invalid JWKSet');
|
||||
}
|
||||
|
||||
return JWKSet::createFromKeyData($json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
PHP JWT Console Component
|
||||
================================
|
||||
|
||||
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
|
||||
|
||||
**Please do not submit any Pull Request here.**
|
||||
You should go to [the main repository](https://github.com/web-token/jwt-framework) instead.
|
||||
|
||||
# Documentation
|
||||
|
||||
The official documentation is available as https://web-token.spomky-labs.com/
|
||||
|
||||
# Licence
|
||||
|
||||
This software is release under [MIT licence](LICENSE).
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use function count;
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Jose\Component\Core\JWK;
|
||||
use Jose\Component\Core\JWKSet;
|
||||
use Jose\Component\Core\Util\JsonConverter;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class RotateKeysetCommand extends ObjectOutputCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:rotate')
|
||||
->setDescription('Rotate a key set.')
|
||||
->setHelp('This command removes the last key in a key set a place a new one at the beginning.')
|
||||
->addArgument('jwkset', InputArgument::REQUIRED, 'The JWKSet object')
|
||||
->addArgument('jwk', InputArgument::REQUIRED, 'The new JWK object')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$jwkset = $this->getKeyset($input)->all();
|
||||
$jwk = $this->getKey($input);
|
||||
|
||||
if (0 !== count($jwkset)) {
|
||||
array_pop($jwkset);
|
||||
}
|
||||
array_unshift($jwkset, $jwk);
|
||||
|
||||
$this->prepareJsonOutput($input, $output, new JWKSet($jwkset));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the keyset is invalid
|
||||
*/
|
||||
private function getKeyset(InputInterface $input): JWKSet
|
||||
{
|
||||
$jwkset = $input->getArgument('jwkset');
|
||||
if (!is_string($jwkset)) {
|
||||
throw new InvalidArgumentException('Invalid JWKSet');
|
||||
}
|
||||
$json = JsonConverter::decode($jwkset);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('Invalid JWKSet');
|
||||
}
|
||||
|
||||
return JWKSet::createFromKeyData($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key is invalid
|
||||
*/
|
||||
private function getKey(InputInterface $input): JWK
|
||||
{
|
||||
$jwk = $input->getArgument('jwk');
|
||||
if (!is_string($jwk)) {
|
||||
throw new InvalidArgumentException('Invalid JWK');
|
||||
}
|
||||
$json = JsonConverter::decode($jwk);
|
||||
if (!is_array($json)) {
|
||||
throw new InvalidArgumentException('Invalid JWK');
|
||||
}
|
||||
|
||||
return new JWK($json);
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class RsaKeyGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:generate:rsa')
|
||||
->setDescription('Generate a RSA key (JWK format)')
|
||||
->addArgument('size', InputArgument::REQUIRED, 'Key size.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key size is invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$size = $input->getArgument('size');
|
||||
if (null === $size) {
|
||||
$size = 1;
|
||||
} elseif (is_array($size)) {
|
||||
$size = 1;
|
||||
} else {
|
||||
$size = (int) $size;
|
||||
}
|
||||
$args = $this->getOptions($input);
|
||||
if ($size < 1) {
|
||||
throw new InvalidArgumentException('Invalid size');
|
||||
}
|
||||
|
||||
$jwk = JWKFactory::createRSAKey($size, $args);
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use Jose\Component\Core\JWKSet;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class RsaKeysetGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:generate:rsa')
|
||||
->setDescription('Generate a key set with RSA keys (JWK format)')
|
||||
->addArgument('quantity', InputArgument::REQUIRED, 'Quantity of keys in the key set.')
|
||||
->addArgument('size', InputArgument::REQUIRED, 'Key size.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the quantity is invalid
|
||||
* @throws InvalidArgumentException if the key size is invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$quantity = $input->getArgument('quantity');
|
||||
if (null === $quantity) {
|
||||
$quantity = 1;
|
||||
} elseif (is_array($quantity)) {
|
||||
$quantity = 1;
|
||||
} else {
|
||||
$quantity = (int) $quantity;
|
||||
}
|
||||
$size = $input->getArgument('size');
|
||||
if (null === $size) {
|
||||
$size = 1;
|
||||
} elseif (is_array($size)) {
|
||||
$size = 1;
|
||||
} else {
|
||||
$size = (int) $size;
|
||||
}
|
||||
if ($quantity < 1) {
|
||||
throw new InvalidArgumentException('Invalid quantity');
|
||||
}
|
||||
if ($size < 1) {
|
||||
throw new InvalidArgumentException('Invalid size');
|
||||
}
|
||||
|
||||
$keyset = new JWKSet([]);
|
||||
for ($i = 0; $i < $quantity; ++$i) {
|
||||
$args = $this->getOptions($input);
|
||||
$keyset = $keyset->with(JWKFactory::createRSAKey($size, $args));
|
||||
}
|
||||
$this->prepareJsonOutput($input, $output, $keyset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_bool;
|
||||
use function is_string;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class SecretKeyGeneratorCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:generate:from_secret')
|
||||
->setDescription('Generate an octet key (JWK format) using an existing secret')
|
||||
->addArgument('secret', InputArgument::REQUIRED, 'The secret')
|
||||
->addOption('is_b64', 'b', InputOption::VALUE_NONE, 'Indicates if the secret is Base64 encoded (useful for binary secrets)')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the secret is invalid
|
||||
* @throws InvalidArgumentException if the option "is_b4" is not a boolean
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$secret = $input->getArgument('secret');
|
||||
if (!is_string($secret)) {
|
||||
throw new InvalidArgumentException('Invalid secret');
|
||||
}
|
||||
$isBsae64Encoded = $input->getOption('is_b64');
|
||||
if (!is_bool($isBsae64Encoded)) {
|
||||
throw new InvalidArgumentException('Invalid option value for "is_b64"');
|
||||
}
|
||||
if ($isBsae64Encoded) {
|
||||
$secret = base64_decode($secret, true);
|
||||
}
|
||||
$args = $this->getOptions($input);
|
||||
|
||||
$jwk = JWKFactory::createFromSecret($secret, $args);
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_string;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class X509CertificateLoaderCommand extends GeneratorCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('key:load:x509')
|
||||
->setDescription('Load a key from a X.509 certificate file.')
|
||||
->addArgument('file', InputArgument::REQUIRED, 'Filename of the X.509 certificate.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the file is invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$file = $input->getArgument('file');
|
||||
if (!is_string($file)) {
|
||||
throw new InvalidArgumentException('Invalid file');
|
||||
}
|
||||
$args = [];
|
||||
foreach (['use', 'alg'] as $key) {
|
||||
$value = $input->getOption($key);
|
||||
if (null !== $value) {
|
||||
$args[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$jwk = JWKFactory::createFromCertificateFile($file, $args);
|
||||
$this->prepareJsonOutput($input, $output, $jwk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_string;
|
||||
use Jose\Component\KeyManagement\X5UFactory;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class X5ULoaderCommand extends ObjectOutputCommand
|
||||
{
|
||||
/**
|
||||
* @var X5UFactory
|
||||
*/
|
||||
private $x5uFactory;
|
||||
|
||||
public function __construct(X5UFactory $x5uFactory, ?string $name = null)
|
||||
{
|
||||
$this->x5uFactory = $x5uFactory;
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
$this
|
||||
->setName('keyset:load:x5u')
|
||||
->setDescription('Loads a key set from an url.')
|
||||
->setHelp('This command will try to get a key set from an URL. The distant key set is list of X.509 certificates.')
|
||||
->addArgument('url', InputArgument::REQUIRED, 'The URL')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the URL is invalid
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$url = $input->getArgument('url');
|
||||
if (!is_string($url)) {
|
||||
throw new InvalidArgumentException('Invalid URL');
|
||||
}
|
||||
$result = $this->x5uFactory->loadFromUrl($url);
|
||||
$this->prepareJsonOutput($input, $output, $result);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "web-token/jwt-console",
|
||||
"description": "Console component of the JWT Framework.",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"],
|
||||
"homepage": "https://github.com/web-token",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Florent Morselli",
|
||||
"homepage": "https://github.com/Spomky"
|
||||
},{
|
||||
"name": "All contributors",
|
||||
"homepage": "https://github.com/web-token/jwt-console/contributors"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jose\\Component\\Console\\": ""
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"symfony/console": "^4.2|^5.0",
|
||||
"web-token/jwt-key-mgmt": "^2.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Contributing
|
||||
|
||||
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
|
||||
Please do not submit any Pull Requests here. It will be automatically closed.
|
||||
@@ -0,0 +1 @@
|
||||
patreon: FlorentMorselli
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Please do not submit any Pull Requests here. It will be automatically closed.
|
||||
|
||||
You should submit it here: https://github.com/web-token/jwt-framework/pulls
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Core;
|
||||
|
||||
interface Algorithm
|
||||
{
|
||||
/**
|
||||
* Returns the name of the algorithm.
|
||||
*/
|
||||
public function name(): string;
|
||||
|
||||
/**
|
||||
* Returns the key types suitable for this algorithm (e.g. "oct", "RSA"...).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function allowedKeyTypes(): array;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Core;
|
||||
|
||||
use function array_key_exists;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class AlgorithmManager
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $algorithms = [];
|
||||
|
||||
/**
|
||||
* @param Algorithm[] $algorithms
|
||||
*/
|
||||
public function __construct(array $algorithms)
|
||||
{
|
||||
foreach ($algorithms as $algorithm) {
|
||||
$this->add($algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the algorithm is supported.
|
||||
*
|
||||
* @param string $algorithm The algorithm
|
||||
*/
|
||||
public function has(string $algorithm): bool
|
||||
{
|
||||
return array_key_exists($algorithm, $this->algorithms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of names of supported algorithms.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function list(): array
|
||||
{
|
||||
return array_keys($this->algorithms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the algorithm if supported, otherwise throw an exception.
|
||||
*
|
||||
* @param string $algorithm The algorithm
|
||||
*
|
||||
* @throws InvalidArgumentException if the algorithm is not supported
|
||||
*/
|
||||
public function get(string $algorithm): Algorithm
|
||||
{
|
||||
if (!$this->has($algorithm)) {
|
||||
throw new InvalidArgumentException(sprintf('The algorithm "%s" is not supported.', $algorithm));
|
||||
}
|
||||
|
||||
return $this->algorithms[$algorithm];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an algorithm to the manager.
|
||||
*/
|
||||
public function add(Algorithm $algorithm): void
|
||||
{
|
||||
$name = $algorithm->name();
|
||||
$this->algorithms[$name] = $algorithm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Core;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function is_string;
|
||||
|
||||
class AlgorithmManagerFactory
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $algorithms = [];
|
||||
|
||||
/**
|
||||
* Adds an algorithm.
|
||||
*
|
||||
* Each algorithm is identified by an alias hence it is allowed to have the same algorithm twice (or more).
|
||||
* This can be helpful when an algorithm have several configuration options.
|
||||
*/
|
||||
public function add(string $alias, Algorithm $algorithm): void
|
||||
{
|
||||
$this->algorithms[$alias] = $algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of aliases.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function aliases(): array
|
||||
{
|
||||
return array_keys($this->algorithms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all algorithms supported by this factory.
|
||||
* This is an associative array. Keys are the aliases of the algorithms.
|
||||
*
|
||||
* @return Algorithm[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->algorithms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an algorithm manager using the given aliases.
|
||||
*
|
||||
* @param string[] $aliases
|
||||
*
|
||||
* @throws InvalidArgumentException if the alias is invalid or is not supported
|
||||
*/
|
||||
public function create(array $aliases): AlgorithmManager
|
||||
{
|
||||
$algorithms = [];
|
||||
foreach ($aliases as $alias) {
|
||||
if (!is_string($alias)) {
|
||||
throw new InvalidArgumentException('Invalid alias');
|
||||
}
|
||||
if (!isset($this->algorithms[$alias])) {
|
||||
throw new InvalidArgumentException(sprintf('The algorithm with the alias "%s" is not supported.', $alias));
|
||||
}
|
||||
$algorithms[] = $this->algorithms[$alias];
|
||||
}
|
||||
|
||||
return new AlgorithmManager($algorithms);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Core;
|
||||
|
||||
use function array_key_exists;
|
||||
use Base64Url\Base64Url;
|
||||
use function in_array;
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use JsonSerializable;
|
||||
|
||||
class JWK implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $values = [];
|
||||
|
||||
/**
|
||||
* Creates a JWK object using the given values.
|
||||
* The member "kty" is mandatory. Other members are NOT checked.
|
||||
*
|
||||
* @throws InvalidArgumentException if the key parameter "kty" is missing
|
||||
*/
|
||||
public function __construct(array $values)
|
||||
{
|
||||
if (!isset($values['kty'])) {
|
||||
throw new InvalidArgumentException('The parameter "kty" is mandatory.');
|
||||
}
|
||||
$this->values = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JWK object using the given Json string.
|
||||
*
|
||||
* @throws InvalidArgumentException if the data is not valid
|
||||
*
|
||||
* @return JWK
|
||||
*/
|
||||
public static function createFromJson(string $json): self
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
if (!is_array($data)) {
|
||||
throw new InvalidArgumentException('Invalid argument.');
|
||||
}
|
||||
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the values to be serialized.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value with a specific key.
|
||||
*
|
||||
* @param string $key The key
|
||||
*
|
||||
* @throws InvalidArgumentException if the key does not exist
|
||||
*
|
||||
* @return null|mixed
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
if (!$this->has($key)) {
|
||||
throw new InvalidArgumentException(sprintf('The value identified by "%s" does not exist.', $key));
|
||||
}
|
||||
|
||||
return $this->values[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the JWK has the value identified by.
|
||||
*
|
||||
* @param string $key The key
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values stored in the JWK object.
|
||||
*
|
||||
* @return array Values of the JWK object
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thumbprint of the key.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7638
|
||||
*
|
||||
* @throws InvalidArgumentException if the hashing function is not supported
|
||||
*/
|
||||
public function thumbprint(string $hash_algorithm): string
|
||||
{
|
||||
if (!in_array($hash_algorithm, hash_algos(), true)) {
|
||||
throw new InvalidArgumentException(sprintf('The hash algorithm "%s" is not supported.', $hash_algorithm));
|
||||
}
|
||||
$values = array_intersect_key($this->values, array_flip(['kty', 'n', 'e', 'crv', 'x', 'y', 'k']));
|
||||
ksort($values);
|
||||
$input = json_encode($values, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return Base64Url::encode(hash($hash_algorithm, $input, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the associated public key.
|
||||
* This method has no effect for:
|
||||
* - public keys
|
||||
* - shared keys
|
||||
* - unknown keys.
|
||||
*
|
||||
* Known keys are "oct", "RSA", "EC" and "OKP".
|
||||
*
|
||||
* @return JWK
|
||||
*/
|
||||
public function toPublic(): self
|
||||
{
|
||||
$values = array_diff_key($this->values, array_flip(['p', 'd', 'q', 'dp', 'dq', 'qi']));
|
||||
|
||||
return new self($values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Core;
|
||||
|
||||
use function array_key_exists;
|
||||
use ArrayIterator;
|
||||
use function count;
|
||||
use Countable;
|
||||
use function in_array;
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
use Traversable;
|
||||
|
||||
class JWKSet implements Countable, IteratorAggregate, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $keys = [];
|
||||
|
||||
/**
|
||||
* @param JWK[] $keys
|
||||
*
|
||||
* @throws InvalidArgumentException if the list is invalid
|
||||
*/
|
||||
public function __construct(array $keys)
|
||||
{
|
||||
foreach ($keys as $k => $key) {
|
||||
if (!$key instanceof JWK) {
|
||||
throw new InvalidArgumentException('Invalid list. Should only contains JWK objects');
|
||||
}
|
||||
|
||||
if ($key->has('kid')) {
|
||||
unset($keys[$k]);
|
||||
$this->keys[$key->get('kid')] = $key;
|
||||
} else {
|
||||
$this->keys[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JWKSet object using the given values.
|
||||
*
|
||||
* @throws InvalidArgumentException if the keyset is not valid
|
||||
*
|
||||
* @return JWKSet
|
||||
*/
|
||||
public static function createFromKeyData(array $data): self
|
||||
{
|
||||
if (!isset($data['keys'])) {
|
||||
throw new InvalidArgumentException('Invalid data.');
|
||||
}
|
||||
if (!is_array($data['keys'])) {
|
||||
throw new InvalidArgumentException('Invalid data.');
|
||||
}
|
||||
|
||||
$jwkset = new self([]);
|
||||
foreach ($data['keys'] as $key) {
|
||||
$jwk = new JWK($key);
|
||||
if ($jwk->has('kid')) {
|
||||
$jwkset->keys[$jwk->get('kid')] = $jwk;
|
||||
} else {
|
||||
$jwkset->keys[] = $jwk;
|
||||
}
|
||||
}
|
||||
|
||||
return $jwkset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JWKSet object using the given Json string.
|
||||
*
|
||||
* @throws InvalidArgumentException if the data is not valid
|
||||
*
|
||||
* @return JWKSet
|
||||
*/
|
||||
public static function createFromJson(string $json): self
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
if (!is_array($data)) {
|
||||
throw new InvalidArgumentException('Invalid argument.');
|
||||
}
|
||||
|
||||
return self::createFromKeyData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of keys stored in the key set.
|
||||
*
|
||||
* @return JWK[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add key to store in the key set.
|
||||
* This method is immutable and will return a new object.
|
||||
*
|
||||
* @return JWKSet
|
||||
*/
|
||||
public function with(JWK $jwk): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
|
||||
if ($jwk->has('kid')) {
|
||||
$clone->keys[$jwk->get('kid')] = $jwk;
|
||||
} else {
|
||||
$clone->keys[] = $jwk;
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove key from the key set.
|
||||
* This method is immutable and will return a new object.
|
||||
*
|
||||
* @param int|string $key Key to remove from the key set
|
||||
*
|
||||
* @return JWKSet
|
||||
*/
|
||||
public function without($key): self
|
||||
{
|
||||
if (!$this->has($key)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
unset($clone->keys[$key]);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the key set contains a key with the given index.
|
||||
*
|
||||
* @param int|string $index
|
||||
*/
|
||||
public function has($index): bool
|
||||
{
|
||||
return array_key_exists($index, $this->keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key with the given index. Throws an exception if the index is not present in the key store.
|
||||
*
|
||||
* @param int|string $index
|
||||
*
|
||||
* @throws InvalidArgumentException if the index is not defined
|
||||
*/
|
||||
public function get($index): JWK
|
||||
{
|
||||
if (!$this->has($index)) {
|
||||
throw new InvalidArgumentException('Undefined index.');
|
||||
}
|
||||
|
||||
return $this->keys[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the values to be serialized.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return ['keys' => array_values($this->keys)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of keys in the key set.
|
||||
*
|
||||
* @param int $mode
|
||||
*/
|
||||
public function count($mode = COUNT_NORMAL): int
|
||||
{
|
||||
return count($this->keys, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find a key that fits on the selected requirements.
|
||||
* Returns null if not found.
|
||||
*
|
||||
* @param string $type Must be 'sig' (signature) or 'enc' (encryption)
|
||||
* @param null|Algorithm $algorithm Specifies the algorithm to be used
|
||||
* @param array $restrictions More restrictions such as 'kid' or 'kty'
|
||||
*
|
||||
* @throws InvalidArgumentException if the key type is not valid (must be "sig" or "enc")
|
||||
*/
|
||||
public function selectKey(string $type, ?Algorithm $algorithm = null, array $restrictions = []): ?JWK
|
||||
{
|
||||
if (!in_array($type, ['enc', 'sig'], true)) {
|
||||
throw new InvalidArgumentException('Allowed key types are "sig" or "enc".');
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($this->keys as $key) {
|
||||
$ind = 0;
|
||||
|
||||
$can_use = $this->canKeyBeUsedFor($type, $key);
|
||||
if (false === $can_use) {
|
||||
continue;
|
||||
}
|
||||
$ind += $can_use;
|
||||
|
||||
$alg = $this->canKeyBeUsedWithAlgorithm($algorithm, $key);
|
||||
if (false === $alg) {
|
||||
continue;
|
||||
}
|
||||
$ind += $alg;
|
||||
|
||||
if (false === $this->doesKeySatisfyRestrictions($restrictions, $key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = ['key' => $key, 'ind' => $ind];
|
||||
}
|
||||
|
||||
if (0 === count($result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
usort($result, [$this, 'sortKeys']);
|
||||
|
||||
return $result[0]['key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method only. Should not be used.
|
||||
*
|
||||
* @internal
|
||||
* @internal
|
||||
*/
|
||||
public static function sortKeys(array $a, array $b): int
|
||||
{
|
||||
if ($a['ind'] === $b['ind']) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ($a['ind'] > $b['ind']) ? -1 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method only. Should not be used.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return new ArrayIterator($this->keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key does not fulfill with the "key_ops" constraint
|
||||
*
|
||||
* @return bool|int
|
||||
*/
|
||||
private function canKeyBeUsedFor(string $type, JWK $key)
|
||||
{
|
||||
if ($key->has('use')) {
|
||||
return $type === $key->get('use') ? 1 : false;
|
||||
}
|
||||
if ($key->has('key_ops')) {
|
||||
$key_ops = $key->get('key_ops');
|
||||
if (!is_array($key_ops)) {
|
||||
throw new InvalidArgumentException('Invalid key parameter "key_ops". Should be a list of key operations');
|
||||
}
|
||||
|
||||
return $type === self::convertKeyOpsToKeyUse($key_ops) ? 1 : false;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|int
|
||||
*/
|
||||
private function canKeyBeUsedWithAlgorithm(?Algorithm $algorithm, JWK $key)
|
||||
{
|
||||
if (null === $algorithm) {
|
||||
return 0;
|
||||
}
|
||||
if (!in_array($key->get('kty'), $algorithm->allowedKeyTypes(), true)) {
|
||||
return false;
|
||||
}
|
||||
if ($key->has('alg')) {
|
||||
return $algorithm->name() === $key->get('alg') ? 2 : false;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private function doesKeySatisfyRestrictions(array $restrictions, JWK $key): bool
|
||||
{
|
||||
foreach ($restrictions as $k => $v) {
|
||||
if (!$key->has($k) || $v !== $key->get($k)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the key operation is not supported
|
||||
*/
|
||||
private static function convertKeyOpsToKeyUse(array $key_ops): string
|
||||
{
|
||||
switch (true) {
|
||||
case in_array('verify', $key_ops, true):
|
||||
case in_array('sign', $key_ops, true):
|
||||
return 'sig';
|
||||
|
||||
case in_array('encrypt', $key_ops, true):
|
||||
case in_array('decrypt', $key_ops, true):
|
||||
case in_array('wrapKey', $key_ops, true):
|
||||
case in_array('unwrapKey', $key_ops, true):
|
||||
case in_array('deriveKey', $key_ops, true):
|
||||
case in_array('deriveBits', $key_ops, true):
|
||||
return 'enc';
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException(sprintf('Unsupported key operation value "%s"', $key_ops));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2020 Spomky-Labs
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Jose\Component\Core;
|
||||
|
||||
interface JWT
|
||||
{
|
||||
/**
|
||||
* Returns the payload of the JWT.
|
||||
* null is a valid payload (e.g. JWS with detached payload).
|
||||
*/
|
||||
public function getPayload(): ?string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2019 Spomky-Labs
|
||||
|
||||
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, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,15 @@
|
||||
PHP JWT Core Component
|
||||
======================
|
||||
|
||||
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
|
||||
|
||||
**Please do not submit any Pull Request here.**
|
||||
You should go to [the main repository](https://github.com/web-token/jwt-framework) instead.
|
||||
|
||||
# Documentation
|
||||
|
||||
The official documentation is available as https://web-token.spomky-labs.com/
|
||||
|
||||
# Licence
|
||||
|
||||
This software is release under [MIT licence](LICENSE).
|
||||
Loaded 100 of 2036 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user