diff --git a/webapp/migrations/Version20240917113927.php b/webapp/migrations/Version20240917113927.php new file mode 100755 index 0000000000..9eea7036b9 --- /dev/null +++ b/webapp/migrations/Version20240917113927.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE problem_attachment_content ADD is_executable TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Whether this file gets an executable bit.\''); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE problem_attachment_content DROP is_executable'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/src/Entity/ProblemAttachmentContent.php b/webapp/src/Entity/ProblemAttachmentContent.php index d961a526e6..7dd60b2c83 100644 --- a/webapp/src/Entity/ProblemAttachmentContent.php +++ b/webapp/src/Entity/ProblemAttachmentContent.php @@ -3,6 +3,7 @@ namespace App\Entity; use Doctrine\ORM\Mapping as ORM; +use JMS\Serializer\Annotation as Serializer; #[ORM\Entity] #[ORM\Table(options: [ @@ -25,6 +26,10 @@ class ProblemAttachmentContent #[ORM\Column(type: 'blobtext', options: ['comment' => 'Attachment content'])] private string $content; + #[ORM\Column(options: ['comment' => 'Whether this file gets an executable bit.', 'default' => 0])] + #[Serializer\Exclude] + private bool $isExecutable = false; + public function getAttachment(): ProblemAttachment { return $this->attachment; @@ -48,4 +53,15 @@ public function setContent(string $content): self return $this; } + + public function setIsExecutable(bool $isExecutable): ProblemAttachmentContent + { + $this->isExecutable = $isExecutable; + return $this; + } + + public function isExecutable(): bool + { + return $this->isExecutable; + } } diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 423f7b3f96..744bfe4201 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -892,6 +892,14 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse foreach ($problem->getProblem()->getAttachments() as $attachment) { $filename = sprintf('%s/attachments/%s', $problem->getShortname(), $attachment->getName()); $zip->addFromString($filename, $attachment->getContent()->getContent()); + if ($attachment->getContent()->isExecutable()) { + // 100755 = regular file, executable + $zip->setExternalAttributesName( + $filename, + ZipArchive::OPSYS_UNIX, + octdec('100755') << 16 + ); + } } } diff --git a/webapp/src/Service/ImportProblemService.php b/webapp/src/Service/ImportProblemService.php index 8b58db4f46..64d5059137 100644 --- a/webapp/src/Service/ImportProblemService.php +++ b/webapp/src/Service/ImportProblemService.php @@ -539,6 +539,14 @@ public function importZippedProblem( continue; } + // In doubt make files executable, but try to read it from the zip file. + $executableBit = true; + if ($zip->getExternalAttributesIndex($j, $opsys, $attr) + && $opsys==ZipArchive::OPSYS_UNIX + && (($attr >> 16) & 0100) === 0) { + $executableBit = false; + } + $name = basename($filename); $fileParts = explode('.', $name); @@ -558,6 +566,10 @@ public function importZippedProblem( $messages['info'][] = sprintf("Updated attachment '%s'", $name); $numAttachments++; } + if ($executableBit !== $attachmentContent->isExecutable()) { + $attachmentContent->setIsExecutable($executableBit); + $messages['info'][] = sprintf("Updated executable bit of attachment '%s'", $name); + } } else { $attachment = new ProblemAttachment(); $attachmentContent = new ProblemAttachmentContent(); @@ -567,7 +579,9 @@ public function importZippedProblem( ->setType($type) ->setContent($attachmentContent); - $attachmentContent->setContent($content); + $attachmentContent + ->setContent($content) + ->setIsExecutable($executableBit); $this->em->persist($attachment);