From 8f0aefc02dbb090d6c87abe7e428e6e663facfb1 Mon Sep 17 00:00:00 2001 From: Nick Huang <9151347+nick322@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:32:30 +0800 Subject: [PATCH] fix(#34): tempnam error --- README.md | 24 +++++++++++- index.php | 26 ++++++++++++- src/Encrypt.php | 33 ++++++++++++---- src/PackageEncryptor.php | 19 +++++----- src/TempFileManager.php | 82 ++++++++++++++++++++++++++++++++++++++++ tests/EncryptorTest.php | 18 ++++++--- 6 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 src/TempFileManager.php diff --git a/README.md b/README.md index b50a533..e130f33 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ secure-spreadsheet run --password=1 --input=/Users/nick/Encryptor/Book1.xlsx --o In php +Usage Examples + ```php require "vendor/autoload.php"; @@ -40,8 +42,8 @@ $test->input('Book1.xlsx') ->output('bb.xlsx'); ``` +Memory-only input and output (no file interaction) -If you want to only use memory/variable output and input, and no file interaction ```php $test = new Encrypt($nofile = true); $output = $test->input($binaryData) @@ -49,6 +51,26 @@ $output = $test->input($binaryData) ->output(); ``` +Memory output with a temporary folder + +```php +$test = new Encrypt(true); +$out = $test->input('Book1.xlsx') + ->setTempPathFolder(__DIR__ . DIRECTORY_SEPARATOR . 'tmp') + ->password('111') + ->output(); +``` + +File input and file output with a temporary folder + +```php +$test = new Encrypt; +$test->input('Book1.xlsx') + ->setTempPathFolder(__DIR__ . DIRECTORY_SEPARATOR . 'tmp') + ->password('111') + ->output('bb4.xlsx'); +``` + ## Credits Thanks to [xlsx-populate](https://github.com/dtjohnson/xlsx-populate) for providing the encryption and password protection. diff --git a/index.php b/index.php index 833db73..eb5f473 100644 --- a/index.php +++ b/index.php @@ -4,7 +4,29 @@ use Nick\SecureSpreadsheet\Encrypt; -$test = new Encrypt(); +// output file +$test = new Encrypt; $test->input('Book1.xlsx') ->password('111') - ->output('bb.xlsx'); + ->output('bb2.xlsx'); + +// output file with nofile +$test = new Encrypt(true); +$out = $test->input('Book1.xlsx') + ->password('111') + ->output(); + +// output file with nofile and set temp path folder +$test = new Encrypt(true); +$out = $test->input('Book1.xlsx') + ->setTempPathFolder(__DIR__.DIRECTORY_SEPARATOR.'tmp') + ->password('111') + ->output(); + + +// output file with set temp path folder +$test = new Encrypt; +$test->input('Book1.xlsx') + ->setTempPathFolder(__DIR__.DIRECTORY_SEPARATOR.'tmp') + ->password('111') + ->output('bb4.xlsx'); diff --git a/src/Encrypt.php b/src/Encrypt.php index 6d4ba3f..4c10a63 100644 --- a/src/Encrypt.php +++ b/src/Encrypt.php @@ -6,13 +6,18 @@ use OLE; use OLE_PPS_File; use OLE_PPS_Root; +use RuntimeException; class Encrypt { private $data; + private $password; + private $noFile = false; + private $tmpPathFolder = null; + public function __construct(bool $nofile = false) { $this->noFile = $nofile; @@ -29,10 +34,10 @@ public function input(string $data) } else { $this->data = function () use ($data) { $fp = fopen($data, 'rb'); - if (!$fp) { + if (! $fp) { throw new Exception('file not found'); } - while (!feof($fp)) { + while (! feof($fp)) { yield unpack('C*', fread($fp, 4096)); } fclose($fp); @@ -45,12 +50,13 @@ public function input(string $data) public function password(string $password) { $this->password = $password; + return $this; } public function output(?string $filePath = null) { - if (!$this->noFile && is_null($filePath)) { + if (! $this->noFile && is_null($filePath)) { throw new Exception('Output Filepath cannot be NULL when NOFILE is False'); } @@ -64,7 +70,8 @@ public function output(?string $filePath = null) $encryptionInfo['package']['blockSize'], $encryptionInfo['package']['saltValue'], $packageKey, - $this->data + $this->data, + $this->tmpPathFolder ); $encryptionInfo['dataIntegrity'] = $this->createDataIntegrity($encryptionInfo, $packageKey, $encryptedPackage['tmpFile']); @@ -103,12 +110,11 @@ public function output(?string $filePath = null) $OLE2->append(pack('C*', ...$unpackEncryptedPackage)); } - unlink($encryptedPackage['tmpFile']); - $root = new OLE_PPS_Root(1000000000, 1000000000, [$OLE, $OLE2]); if ($this->noFile) { - $filePath = tempnam(sys_get_temp_dir(), 'NOFILE'); + $tmp = new TempFileManager($this->tmpPathFolder); + $filePath = $tmp->path('NOFILE'); } $root->save($filePath); @@ -201,4 +207,15 @@ private function addVerifierHash(array &$encryptionInfo) $verifierHashValue ); } -} \ No newline at end of file + + public function setTempPathFolder(string $path) + { + if (! is_writable($path)) { + throw new RuntimeException('Temp dir not writable.'); + } + + $this->tmpPathFolder = $path; + + return $this; + } +} diff --git a/src/PackageEncryptor.php b/src/PackageEncryptor.php index ce11440..9b8994f 100644 --- a/src/PackageEncryptor.php +++ b/src/PackageEncryptor.php @@ -11,11 +11,15 @@ public static function encrypt( int $blockSize, array $saltValue, array $key, - callable $input + callable $input, + ?string $tmpPathFolder = null ) { - $tmpOutputChunk = tempnam(sys_get_temp_dir(), 'outputChunk'); - $tmpFileHeaderLength = tempnam(sys_get_temp_dir(), 'fileHeaderLength'); - $tmpFile = tempnam(sys_get_temp_dir(), 'file'); + + $tmp = new TempFileManager($tmpPathFolder); + + $tmpOutputChunk = $tmp->path('outputChunk'); + $tmpFileHeaderLength = $tmp->path('fileHeaderLength'); + $tmpFile = $tmp->path('file'); if (is_callable($input) && is_a($in = $input(), 'Generator')) { $inputCount = 0; @@ -35,14 +39,11 @@ public static function encrypt( } file_put_contents($tmpFileHeaderLength, pack('C*', ...CryptoHelper::createUInt32LEBuffer($inputCount, EncryptionConfig::PACKAGE_OFFSET))); - file_put_contents($tmpFile, file_get_contents($tmpFileHeaderLength) . file_get_contents($tmpOutputChunk)); - - unlink($tmpOutputChunk); - unlink($tmpFileHeaderLength); + file_put_contents($tmpFile, file_get_contents($tmpFileHeaderLength).file_get_contents($tmpOutputChunk)); return ['tmpFile' => $tmpFile]; } return []; } -} \ No newline at end of file +} diff --git a/src/TempFileManager.php b/src/TempFileManager.php new file mode 100644 index 0000000..17384fb --- /dev/null +++ b/src/TempFileManager.php @@ -0,0 +1,82 @@ +baseDir = $this->resolveBaseDir($preferredDir); + $this->jobDir = $this->createJobDir(); + + // request shutdown function to cleanup temp files + register_shutdown_function([$this, 'cleanup']); + } + + private function resolveBaseDir(?string $preferredDir): string + { + $candidates = array_filter([ + $preferredDir, + getenv('TMPDIR'), + getenv('TMP'), + getenv('TEMP'), + sys_get_temp_dir(), + '/tmp', + getcwd().DIRECTORY_SEPARATOR.'tmp', + ]); + + foreach ($candidates as $dir) { + if ($this->isUsableDir($dir)) { + return rtrim($dir, DIRECTORY_SEPARATOR); + } + } + + throw new RuntimeException('No usable temp directory.'); + } + + private function isUsableDir(string $dir): bool + { + return is_dir($dir) + && is_writable($dir) + && ! is_link($dir); + } + + private function createJobDir(): string + { + $jobDir = $this->baseDir.DIRECTORY_SEPARATOR.'job_'.bin2hex(random_bytes(8)); + + if (! mkdir($jobDir, 0700)) { + throw new RuntimeException('Failed to create job temp directory'); + } + + return $jobDir; + } + + public function path(string $name): string + { + return $this->jobDir.DIRECTORY_SEPARATOR.$name; + } + + public function cleanup(): void + { + if ($this->cleaned || ! is_dir($this->jobDir)) { + return; + } + + $files = glob($this->jobDir.DIRECTORY_SEPARATOR.'*') ?: []; + foreach ($files as $file) { + @unlink($file); + } + + @rmdir($this->jobDir); + $this->cleaned = true; + } +} diff --git a/tests/EncryptorTest.php b/tests/EncryptorTest.php index 6eabea4..f4e9525 100644 --- a/tests/EncryptorTest.php +++ b/tests/EncryptorTest.php @@ -2,23 +2,25 @@ namespace Illuminate\Tests\Auth; -use PHPUnit\Framework\TestCase; use Nick\SecureSpreadsheet\Encrypt; +use PHPUnit\Framework\TestCase; class EncryptorTest extends TestCase { protected function setUp(): void { - if (file_exists('bb.xlsx')) unlink('bb.xlsx'); + if (file_exists('bb.xlsx')) { + unlink('bb.xlsx'); + } } - public function testEncryptor() + public function test_encryptor() { - (new Encrypt())->input('Book1.xlsx')->password('111')->output('bb.xlsx'); + (new Encrypt)->input('Book1.xlsx')->password('111')->output('bb.xlsx'); $this->assertFileExists('bb.xlsx'); } - public function testEncryptorWithBinaryData() + public function test_encryptor_with_binary_data() { $data = 'Book1.xlsx'; $fp = fopen($data, 'rb'); @@ -27,4 +29,10 @@ public function testEncryptorWithBinaryData() $str = (new Encrypt($nofile = true))->input($binaryData)->password('111')->output(); $this->assertEquals(12288, strlen($str)); } + + public function test_encryptor_with_set_temp_path_folder() + { + (new Encrypt)->input('Book1.xlsx')->setTempPathFolder('/tmp')->password('111')->output('bb.xlsx'); + $this->assertFileExists('bb.xlsx'); + } }