diff --git a/CHANGELOG.md b/CHANGELOG.md index ff995a0d3..0b49db229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ - Chg #889: Update `AbstractDMLQueryBuilder::insertBatch()` method (@Tigrov) - Enh #890: Add properties of `AbstractColumnSchema` class to constructor (@Tigrov) - New #899: Add `ColumnSchemaInterface::hasDefaultValue()` and `ColumnSchemaInterface::null()` methods (@Tigrov) +- New #902: Add `QueryBuilderInterface::prepareParam()` and `QueryBuilderInterface::prepareValue()` methods (@Tigrov) +- Enh #902: Refactor `Quoter::quoteValue()` method (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/UPGRADE.md b/UPGRADE.md index 2b6379bbc..c03633004 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -107,7 +107,9 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace - `QuoterInterface::getRawTableName()` - returns the raw table name without quotes; - `SchemaInterface::getColumnFactory()` - returns the column factory object for concrete DBMS; -- `QueryBuilderInterface::buildColumnDefinition()` - builds column definition for `CREATE TABLE` statement. +- `QueryBuilderInterface::buildColumnDefinition()` - builds column definition for `CREATE TABLE` statement; +- `QueryBuilderInterface::prepareParam()` - converts a `ParamInterface` object to its SQL representation; +- `QueryBuilderInterface::prepareValue()` - converts a value to its SQL representation; ### Remove methods @@ -146,3 +148,4 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace - Allow `ExpressionInterface` for `$alias` parameter of `QueryPartsInterface::withQuery()` method; - Allow `QueryInterface::one()` to return an object; - Allow `QueryInterface::all()` to return array of objects; +- Change `Quoter::quoteValue()` parameter type and return type from `mixed` to `string`; diff --git a/src/Command/AbstractCommand.php b/src/Command/AbstractCommand.php index 6f97718a8..27f973813 100644 --- a/src/Command/AbstractCommand.php +++ b/src/Command/AbstractCommand.php @@ -7,8 +7,6 @@ use Closure; use Throwable; use Yiisoft\Db\Exception\Exception; -use Yiisoft\Db\Expression\Expression; -use Yiisoft\Db\Constant\GettypeResult; use Yiisoft\Db\Query\Data\DataReaderInterface; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\DMLQueryBuilderInterface; @@ -17,14 +15,12 @@ use function explode; use function get_resource_type; -use function gettype; use function is_array; use function is_int; use function is_resource; use function is_scalar; use function is_string; use function preg_replace_callback; -use function str_starts_with; use function stream_get_contents; /** @@ -351,28 +347,14 @@ public function getRawSql(): string } $params = []; - $quoter = $this->getQueryBuilder()->quoter(); + $queryBuilder = $this->getQueryBuilder(); foreach ($this->params as $name => $param) { - if (is_string($name) && !str_starts_with($name, ':')) { + if (is_string($name) && $name[0] !== ':') { $name = ':' . $name; } - $value = $param->getValue(); - - $params[$name] = match ($param->getType()) { - DataType::INTEGER => (string) (int) $value, - DataType::STRING, DataType::LOB => match (gettype($value)) { - GettypeResult::RESOURCE => $name, - GettypeResult::DOUBLE => (string) $value, - default => $value instanceof Expression - ? (string) $value - : $quoter->quoteValue((string) $value), - }, - DataType::BOOLEAN => $value ? 'TRUE' : 'FALSE', - DataType::NULL => 'NULL', - default => $name, - }; + $params[$name] = $queryBuilder->prepareParam($param); } /** @var string[] $params */ diff --git a/src/Command/CommandInterface.php b/src/Command/CommandInterface.php index a10748f90..521ffeed5 100644 --- a/src/Command/CommandInterface.php +++ b/src/Command/CommandInterface.php @@ -213,6 +213,8 @@ public function insertBatch(string $table, iterable $rows, array $columns = []): * @param int|null $length The length of the data type. * @param mixed|null $driverOptions The driver-specific options. * + * @psalm-param DataType::*|null $dataType + * * @throws Exception */ public function bindParam( @@ -244,6 +246,8 @@ public function addUnique(string $table, string $name, array|string $columns): s * @param mixed $value The value to bind to the parameter. * @param int|null $dataType The {@see DataType SQL data type} of the parameter. If null, the type is determined * by the PHP type of the value. + * + * @psalm-param DataType::*|null $dataType */ public function bindValue(int|string $name, mixed $value, int $dataType = null): static; diff --git a/src/Command/Param.php b/src/Command/Param.php index d7f9e3630..a521f6046 100644 --- a/src/Command/Param.php +++ b/src/Command/Param.php @@ -18,10 +18,16 @@ */ final class Param implements ParamInterface, ExpressionInterface { + /** + * @psalm-param DataType::* $type + */ public function __construct(private mixed $value, private int $type) { } + /** + * @psalm-return DataType::* + */ public function getType(): int { return $this->type; diff --git a/src/Command/ParamInterface.php b/src/Command/ParamInterface.php index 68ae5d75d..da17a6347 100644 --- a/src/Command/ParamInterface.php +++ b/src/Command/ParamInterface.php @@ -13,11 +13,15 @@ interface ParamInterface * @param mixed $value The value to bind to the parameter. * @param int $type The SQL data type of the parameter. * If `null`, the type is determined by the PHP type of the value. + * + * @psalm-param DataType::* $type */ public function __construct(mixed $value, int $type); /** * @return int The SQL data type of the parameter. + * + * @psalm-return DataType::* */ public function getType(): int; diff --git a/src/Constant/GettypeResult.php b/src/Constant/GettypeResult.php index ad5ec397c..69ce962fb 100644 --- a/src/Constant/GettypeResult.php +++ b/src/Constant/GettypeResult.php @@ -39,6 +39,10 @@ final class GettypeResult * Define the php type as `resource`. */ public const RESOURCE = 'resource'; + /** + * Define the php type as `resource (closed)`. + */ + public const RESOURCE_CLOSED = 'resource (closed)'; /** * Define the php type as `string`. */ diff --git a/src/QueryBuilder/AbstractColumnDefinitionBuilder.php b/src/QueryBuilder/AbstractColumnDefinitionBuilder.php index dee3a1424..23e7c49bf 100644 --- a/src/QueryBuilder/AbstractColumnDefinitionBuilder.php +++ b/src/QueryBuilder/AbstractColumnDefinitionBuilder.php @@ -5,11 +5,8 @@ namespace Yiisoft\Db\QueryBuilder; use Yiisoft\Db\Constant\ColumnType; -use Yiisoft\Db\Constant\GettypeResult; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; -use function gettype; use function in_array; use function strtolower; @@ -125,45 +122,22 @@ protected function buildDefault(ColumnSchemaInterface $column): string return ' DEFAULT ' . static::GENERATE_UUID_EXPRESSION; } - if ($column->isAutoIncrement() && $column->getType() !== ColumnType::UUID) { + if ($column->isAutoIncrement() && $column->getType() !== ColumnType::UUID + || !$column->hasDefaultValue() + ) { return ''; } - $defaultValue = $this->buildDefaultValue($column); + $defaultValue = $column->dbTypecast($column->getDefaultValue()); + $defaultValue = $this->queryBuilder->prepareValue($defaultValue); - if ($defaultValue === null) { + if ($defaultValue === '') { return ''; } return " DEFAULT $defaultValue"; } - /** - * Return the default value for the column. - * - * @return string|null string with default value of column. - */ - protected function buildDefaultValue(ColumnSchemaInterface $column): string|null - { - if (!$column->hasDefaultValue()) { - return null; - } - - $value = $column->dbTypecast($column->getDefaultValue()); - - /** @var string */ - return match (gettype($value)) { - GettypeResult::INTEGER => (string) $value, - GettypeResult::DOUBLE => (string) $value, - GettypeResult::BOOLEAN => $value ? 'TRUE' : 'FALSE', - GettypeResult::NULL => $column->isNotNull() !== true ? 'NULL' : null, - GettypeResult::OBJECT => $value instanceof ExpressionInterface - ? $this->queryBuilder->buildExpression($value) - : $this->queryBuilder->quoter()->quoteValue((string) $value), - default => $this->queryBuilder->quoter()->quoteValue((string) $value), - }; - } - /** * Builds the custom string that's appended to column definition. * diff --git a/src/QueryBuilder/AbstractDDLQueryBuilder.php b/src/QueryBuilder/AbstractDDLQueryBuilder.php index 7365d5967..98a23ee7a 100644 --- a/src/QueryBuilder/AbstractDDLQueryBuilder.php +++ b/src/QueryBuilder/AbstractDDLQueryBuilder.php @@ -57,7 +57,7 @@ public function addCommentOnColumn(string $table, string $column, string $commen . '.' . $this->quoter->quoteColumnName($column) . ' IS ' - . (string) $this->quoter->quoteValue($comment); + . $this->quoter->quoteValue($comment); } public function addCommentOnTable(string $table, string $comment): string @@ -65,7 +65,7 @@ public function addCommentOnTable(string $table, string $comment): string return 'COMMENT ON TABLE ' . $this->quoter->quoteTableName($table) . ' IS ' - . (string) $this->quoter->quoteValue($comment); + . $this->quoter->quoteValue($comment); } public function addDefaultValue(string $table, string $name, string $column, mixed $value): string @@ -195,10 +195,8 @@ public function createView(string $viewName, QueryInterface|string $subQuery): s if ($subQuery instanceof QueryInterface) { [$rawQuery, $params] = $this->queryBuilder->build($subQuery); - /** @psalm-var mixed $value */ foreach ($params as $key => $value) { - /** @psalm-var mixed */ - $params[$key] = $this->quoter->quoteValue($value); + $params[$key] = $this->queryBuilder->prepareValue($value); } $subQuery = strtr($rawQuery, $params); diff --git a/src/QueryBuilder/AbstractQueryBuilder.php b/src/QueryBuilder/AbstractQueryBuilder.php index a8a4a9703..2dc0c5df5 100644 --- a/src/QueryBuilder/AbstractQueryBuilder.php +++ b/src/QueryBuilder/AbstractQueryBuilder.php @@ -5,7 +5,12 @@ namespace Yiisoft\Db\QueryBuilder; use Yiisoft\Db\Command\CommandInterface; +use Yiisoft\Db\Command\DataType; +use Yiisoft\Db\Command\ParamInterface; use Yiisoft\Db\Connection\ConnectionInterface; +use Yiisoft\Db\Constant\GettypeResult; +use Yiisoft\Db\Exception\InvalidArgumentException; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\Condition\Interface\ConditionInterface; @@ -14,10 +19,14 @@ use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; +use function bin2hex; use function count; +use function get_resource_type; +use function gettype; use function is_string; use function preg_match; use function preg_replace; +use function stream_get_contents; /** * Builds a SELECT SQL statement based on the specification given as a {@see QueryInterface} object. @@ -37,6 +46,16 @@ abstract class AbstractQueryBuilder implements QueryBuilderInterface * The prefix for automatically generated query binding parameters. */ public const PARAM_PREFIX = ':qp'; + + /** + * @var string SQL value of the PHP `false` value. + */ + protected const FALSE_VALUE = 'FALSE'; + /** + * @var string SQL value of the PHP `true` value. + */ + protected const TRUE_VALUE = 'TRUE'; + /** * @psalm-var string[] The abstract column types mapped to physical column types. * @@ -382,6 +401,36 @@ public function quoter(): QuoterInterface return $this->quoter; } + public function prepareParam(ParamInterface $param): string + { + return match ($param->getType()) { + DataType::BOOLEAN => $param->getValue() ? static::TRUE_VALUE : static::FALSE_VALUE, + DataType::INTEGER => (string) (int) $param->getValue(), + DataType::LOB => $this->prepareBinary((string) $param->getValue()), + DataType::NULL => 'NULL', + default => $this->prepareValue($param->getValue()), + }; + } + + public function prepareValue(mixed $value): string + { + /** @psalm-suppress MixedArgument */ + return match (gettype($value)) { + GettypeResult::BOOLEAN => $value ? static::TRUE_VALUE : static::FALSE_VALUE, + GettypeResult::DOUBLE => (string) $value, + GettypeResult::INTEGER => (string) $value, + GettypeResult::NULL => 'NULL', + GettypeResult::OBJECT => match (true) { + $value instanceof Expression => (string) $value, + $value instanceof ParamInterface => $this->prepareParam($value), + default => $this->quoter->quoteValue((string) $value), + }, + GettypeResult::RESOURCE => $this->prepareResource($value), + GettypeResult::RESOURCE_CLOSED => throw new InvalidArgumentException('Resource is closed.'), + default => $this->quoter->quoteValue((string) $value), + }; + } + public function renameColumn(string $table, string $oldName, string $newName): string { return $this->ddlBuilder->renameColumn($table, $oldName, $newName); @@ -435,4 +484,26 @@ public function upsert( ): string { return $this->dmlBuilder->upsert($table, $insertColumns, $updateColumns, $params); } + + /** + * Converts a resource value to its SQL representation or throws an exception if conversion is not possible. + * + * @param resource $value + */ + protected function prepareResource(mixed $value): string + { + if (get_resource_type($value) !== 'stream') { + throw new InvalidArgumentException('Supported only stream resource type.'); + } + + return $this->prepareBinary(stream_get_contents($value)); + } + + /** + * Converts a binary value to its SQL representation using hexadecimal encoding. + */ + protected function prepareBinary(string $binary): string + { + return '0x' . bin2hex($binary); + } } diff --git a/src/QueryBuilder/QueryBuilderInterface.php b/src/QueryBuilder/QueryBuilderInterface.php index e9cb64453..c182a58bc 100644 --- a/src/QueryBuilder/QueryBuilderInterface.php +++ b/src/QueryBuilder/QueryBuilderInterface.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\QueryBuilder; +use Yiisoft\Db\Command\ParamInterface; use Yiisoft\Db\Connection\ConnectionInterface; use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Expression\ExpressionBuilderInterface; @@ -112,4 +113,16 @@ public function getExpressionBuilder(ExpressionInterface $expression): object; * @return QuoterInterface The quoter instance. */ public function quoter(): QuoterInterface; + + /** + * Converts a {@see ParamInterface} object to its SQL representation and quotes it if necessary. + * Used when the bind parameter cannot be used in the SQL query. + */ + public function prepareParam(ParamInterface $param): string; + + /** + * Converts a value to its SQL representation and quotes it if necessary. + * Used when the bind parameter cannot be used in the SQL query. + */ + public function prepareValue(mixed $value): string; } diff --git a/src/Schema/Quoter.php b/src/Schema/Quoter.php index 1c130a9d6..1d3e88403 100644 --- a/src/Schema/Quoter.php +++ b/src/Schema/Quoter.php @@ -207,12 +207,8 @@ public function quoteTableName(string $name): string return implode('.', $parts); } - public function quoteValue(mixed $value): mixed + public function quoteValue(string $value): string { - if (!is_string($value)) { - return $value; - } - return "'" . str_replace("'", "''", addcslashes($value, "\000\032")) . "'"; } diff --git a/src/Schema/QuoterInterface.php b/src/Schema/QuoterInterface.php index 7f2325928..258dcc598 100644 --- a/src/Schema/QuoterInterface.php +++ b/src/Schema/QuoterInterface.php @@ -136,15 +136,14 @@ public function quoteTableName(string $name): string; /** * Quotes a string value for use in a query. * - * Note: That if the parameter isn't a string, it will be returned without change. * Attention: The usage of this method isn't safe. * Use prepared statements. * - * @param mixed $value The value to quote. + * @param string $value The value to quote. * - * @return mixed The quoted value. + * @return string The quoted value. */ - public function quoteValue(mixed $value): mixed; + public function quoteValue(string $value): string; /** * Unquotes a simple column name. diff --git a/src/Schema/SchemaInterface.php b/src/Schema/SchemaInterface.php index 3e774b8a8..2632f16e4 100644 --- a/src/Schema/SchemaInterface.php +++ b/src/Schema/SchemaInterface.php @@ -302,7 +302,7 @@ public function getDefaultSchema(): string|null; * * @return int The type. * - * @see DataType + * @psalm-return DataType::* */ public function getDataType(mixed $data): int; diff --git a/tests/AbstractQueryBuilderTest.php b/tests/AbstractQueryBuilderTest.php index 91c73468c..1c42cf3b3 100644 --- a/tests/AbstractQueryBuilderTest.php +++ b/tests/AbstractQueryBuilderTest.php @@ -6,6 +6,7 @@ use Closure; use JsonException; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; use stdClass; use Throwable; @@ -28,6 +29,7 @@ use Yiisoft\Db\Schema\Builder\ColumnInterface; use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; use Yiisoft\Db\Schema\QuoterInterface; +use Yiisoft\Db\Tests\Provider\QueryBuilderProvider; use Yiisoft\Db\Tests\Support\Assert; use Yiisoft\Db\Tests\Support\DbHelper; use Yiisoft\Db\Tests\Support\TestTrait; @@ -2404,7 +2406,7 @@ public function testOverrideParameters2(): void ); } - /** @dataProvider \Yiisoft\Db\Tests\Provider\QueryBuilderProvider::buildColumnDefinition() */ + #[DataProviderExternal(QueryBuilderProvider::class, 'buildColumnDefinition')] public function testBuildColumnDefinition(string $expected, ColumnSchemaInterface|string $column): void { $db = $this->getConnection(); @@ -2412,4 +2414,23 @@ public function testBuildColumnDefinition(string $expected, ColumnSchemaInterfac $this->assertSame($expected, $qb->buildColumnDefinition($column)); } + + #[DataProviderExternal(QueryBuilderProvider::class, 'prepareParam')] + public function testPrepareParam(string $expected, mixed $value, int $type): void + { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $param = new Param($value, $type); + $this->assertSame($expected, $qb->prepareParam($param)); + } + + #[DataProviderExternal(QueryBuilderProvider::class, 'prepareValue')] + public function testPrepareValue(string $expected, mixed $value): void + { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $this->assertSame($expected, $qb->prepareValue($value)); + } } diff --git a/tests/Db/QueryBuilder/QueryBuilderTest.php b/tests/Db/QueryBuilder/QueryBuilderTest.php index 8c163561c..b48122be0 100644 --- a/tests/Db/QueryBuilder/QueryBuilderTest.php +++ b/tests/Db/QueryBuilder/QueryBuilderTest.php @@ -19,6 +19,10 @@ use Yiisoft\Db\Tests\Support\Stub\Schema; use Yiisoft\Db\Tests\Support\TestTrait; +use function fclose; +use function fopen; +use function stream_context_create; + /** * @group db * @@ -314,4 +318,27 @@ public function testUpsertExecute( $actualParams = []; $actualSQL = $db->getQueryBuilder()->upsert($table, $insertColumns, $updateColumns, $actualParams); } + + public function testPrepareValueClosedResource(): void + { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $this->expectExceptionObject(new InvalidArgumentException('Resource is closed.')); + + $resource = fopen('php://memory', 'r'); + fclose($resource); + + $qb->prepareValue($resource); + } + + public function testPrepareValueNonStreamResource(): void + { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $this->expectExceptionObject(new InvalidArgumentException('Supported only stream resource type.')); + + $qb->prepareValue(stream_context_create()); + } } diff --git a/tests/Db/Schema/QuoterTest.php b/tests/Db/Schema/QuoterTest.php index 3d7bd8599..2bff15ea6 100644 --- a/tests/Db/Schema/QuoterTest.php +++ b/tests/Db/Schema/QuoterTest.php @@ -40,16 +40,6 @@ public function testQuoteTableNameWithSchema(): void $this->assertSame('`schema`.`table`', $quoter->quoteTableName('schema.table')); } - public function testQuoteValueNotString(): void - { - $quoter = new Quoter('`', '`'); - - $this->assertFalse($quoter->quoteValue(false)); - $this->assertTrue($quoter->quoteValue(true)); - $this->assertSame(1, $quoter->quoteValue(1)); - $this->assertSame([], $quoter->quoteValue([])); - } - public function testUnquoteSimpleColumnNameWithStartingCharacterEndingCharacterEquals(): void { $quoter = new Quoter('`', '`'); diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 4bb133c9a..53021b9db 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -20,10 +20,13 @@ use Yiisoft\Db\Schema\Column\ColumnBuilder; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Tests\Support\DbHelper; +use Yiisoft\Db\Tests\Support\Stringable; use Yiisoft\Db\Tests\Support\Stub\Column; use Yiisoft\Db\Tests\Support\TestTrait; use Yiisoft\Db\Tests\Support\TraversableObject; +use function fopen; + /** * @psalm-suppress MixedAssignment * @psalm-suppress MixedArgument @@ -1664,7 +1667,7 @@ public static function buildColumnDefinition(): array "defaultValue('')" => ["varchar(255) DEFAULT ''", ColumnBuilder::string()->defaultValue('')], 'defaultValue(null)' => ['varchar(255) DEFAULT NULL', ColumnBuilder::string()->defaultValue(null)], 'defaultValue($expression)' => ['integer DEFAULT (1 + 2)', ColumnBuilder::integer()->defaultValue(new Expression('(1 + 2)'))], - 'notNull()->defaultValue(null)' => ['varchar(255) NOT NULL', ColumnBuilder::string()->notNull()->defaultValue(null)], + 'defaultValue($emptyExpression)' => ['integer', ColumnBuilder::integer()->defaultValue(new Expression(''))], "integer()->defaultValue('')" => ['integer DEFAULT NULL', ColumnBuilder::integer()->defaultValue('')], 'notNull()' => ['varchar(255) NOT NULL', ColumnBuilder::string()->notNull()], 'null()' => ['varchar(255) NULL', ColumnBuilder::string()->null()], @@ -1694,4 +1697,36 @@ public static function buildColumnDefinition(): array ], ]; } + + public static function prepareParam(): array + { + return [ + 'null' => ['NULL', null, DataType::NULL], + 'true' => ['TRUE', true, DataType::BOOLEAN], + 'false' => ['FALSE', false, DataType::BOOLEAN], + 'integer' => ['1', 1, DataType::INTEGER], + 'integerString' => ['1', '1 or 1=1', DataType::INTEGER], + 'float' => ['1.1', 1.1, DataType::STRING], + 'string' => ["'string'", 'string', DataType::STRING], + 'binary' => ['0x737472696e67', 'string', DataType::LOB], + ]; + } + + public static function prepareValue(): array + { + return [ + 'null' => ['NULL', null], + 'true' => ['TRUE', true], + 'false' => ['FALSE', false], + 'integer' => ['1', 1], + 'float' => ['1.1', 1.1], + 'string' => ["'string'", 'string'], + 'binary' => ['0x737472696e67', fopen(__DIR__ . '/../Support/string.txt', 'rb')], + 'paramBinary' => ['0x737472696e67', new Param('string', DataType::LOB)], + 'paramString' => ["'string'", new Param('string', DataType::STRING)], + 'paramInteger' => ['1', new Param(1, DataType::INTEGER)], + 'expression' => ['(1 + 2)', new Expression('(1 + 2)')], + 'Stringable' => ["'string'", new Stringable('string')], + ]; + } } diff --git a/tests/Support/string.txt b/tests/Support/string.txt new file mode 100644 index 000000000..ec186f1f3 --- /dev/null +++ b/tests/Support/string.txt @@ -0,0 +1 @@ +string \ No newline at end of file