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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions bin/pc2submit
Original file line number Diff line number Diff line change
Expand Up @@ -522,12 +522,15 @@ def do_api_submit():

if team_id :
jsonSub['team_id'] = team_id

zip = io.BytesIO()

with zipfile.ZipFile(zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
for filename in filenames:
arcname = os.path.basename(filename)
zipf.write(filename, arcname)

jsonSub['files'] = []
for filename in filenames :
with open(filename, 'rb') as file:
data = base64.b64encode(file.read()).decode("utf-8")
jsonSub['files'].append({ 'data' : data, 'filename' : filename })
jsonSub['files'] = [{ 'data' : base64.b64encode(zip.getvalue()).decode("utf-8"), 'mime' : 'application/zip' }]

logging.debug(f"API Post info: jsonSub: {jsonSub}")

Expand Down
3 changes: 3 additions & 0 deletions src/edu/csus/ecs/pc2/clics/API202306/CLICSContestInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ public CLICSContestInfo(IInternalContest model, ContestInformation ci) {
}
}
penalty_time = Integer.valueOf(ci.getScoringProperties().getProperty(DefaultScoringAlgorithm.POINTS_PER_NO, "20"));
if(ci.getThawed() != null) {
scoreboard_thaw_time = Utilities.getIso8601formatterWithMS().format(ci.getThawed());
}
scoreboard_type = "pass-fail";
}

Expand Down
20 changes: 16 additions & 4 deletions src/edu/csus/ecs/pc2/clics/API202306/ContestService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import java.io.IOException;
import java.text.ParseException;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Map;

Expand Down Expand Up @@ -420,11 +421,22 @@ private Response HandleContestThawTime(SecurityContext sc, String contestId, Str
// thaw time present, validate now
GregorianCalendar thawTime = getDate(contestId, thawTimeValue);
if (thawTime != null) {
// Set thaw time to this time.
// TODO: tell PC2 to thaw the contest at the given time.
controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": setting of contest thaw time is not implemented");
return Response.status(Status.NOT_MODIFIED).entity("Unable to set contest thaw time to " + thawTime.toString()).build();
Date thawDate = thawTime.getTime();
String thawStr = Utilities.getIso8601formatterWithMS().format(thawDate);
// get the local model's ContestInformation sincd we are modifying the thaw time
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(trivial) typo: "sincd" -> "since"

ContestInformation ci = model.getContestInformation();
if (ci != null) {
// set the new start date/time into the ContestInformation
ci.setThawed(thawDate);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preceding comment says we are setting the "new start date/time", but I don't think that's what's actually being set here. (Copy/paste without an update?)

// tell the Controller to update the ContestInformation, eg the thaw time in this case
controller.updateContestInformation(ci);
controller.getLog().log(Log.INFO, LOG_PREFIX + contestId + ": setting contest thaw time to " + thawStr);
return Response.ok().entity("Contest thaw time set to " + thawStr).build();
}
controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": can not get contest information to set thaw time");
return Response.status(Status.NOT_MODIFIED).entity("Unable to set contest thaw time to " + thawStr).build();
}
controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": bad value for contest thaw time: " + thawTimeValue);
return Response.status(Status.BAD_REQUEST).entity("Bad value for contest thaw time request").build();

}
Expand Down
53 changes: 37 additions & 16 deletions src/edu/csus/ecs/pc2/clics/API202306/SubmissionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

import edu.csus.ecs.pc2.convert.EventFeedUtilities;
import edu.csus.ecs.pc2.core.IInternalController;
import edu.csus.ecs.pc2.core.Utilities;
import edu.csus.ecs.pc2.core.exception.SubmissionRejectedException;
Expand Down Expand Up @@ -607,24 +608,44 @@ public synchronized Response addNewSubmission(@Context HttpServletRequest servle
return Response.status(Response.Status.BAD_REQUEST).entity("no file specified").build();
}

List<IFile> srcFiles = new ArrayList<IFile>();
for(CLICSFileReference file : files) {
String fileName = file.getFilename();
if("".equals(fileName)) {
return Response.status(Response.Status.BAD_REQUEST).entity("no file name specified").build();
List<IFile> srcFiles;

// Backward compatability test: If no mime property specified on the first file, then the CLICSFileReference's are
// actual files and not a zip file. This is in here because initially, due to a misinterpretation of the CLICS
// 2023-06 specification, the command line submit utility (a.k.a. pc2submit) did not put the files in a zip file,
// rather, it created an array of base64 encoded file contents. The CLICS spec has since been clarified but for now,
// we will still support the incorrect implementation as well as the correct one.
CLICSFileReference firstFile = files[0];
String mimeType = firstFile.getMime();

// TODO: deprecate this "if" part and leave the "else" part. JB
if(mimeType == null || "".equals(mimeType)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A zip with an empty mime is also allowed according to the spec, I think that's not handled now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you take a look at the comments in the PR, we are using the absence of the mime-type as a (temporary) indicator that the older version of pc2submit (submit) that did not zip the files is being used. We are keeping this in for now, since some images out there (ie the icpc2025 image) have the version of pc2submit that does not zip. It also does not include the mime-type, so that's how we can tell (for now). The only command line submit utilities that interact with the API are pc2submit (which has been fixed to add the mime-type and zip the files), and the playback utility, which is a private test utility.

After the next contest image cycle, we will remove the deprecated code and use the absence of the mime-type to indicate it's a zip file, as per the specification.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!

srcFiles = new ArrayList<IFile>();
for(CLICSFileReference file : files) {
String fileName = file.getFilename();
if("".equals(fileName)) {
return Response.status(Response.Status.BAD_REQUEST).entity("no file name specified").build();
}
// allow contestant submission of a zero length file. This will generate a CE (hopefully).
// if the following code is uncommented, the submission is not made and a 400 is returned to the submitter.
// it appears that other CCS's allow zero length submissions. *sigh* -- JB
String fileData = file.getData();
if(fileData == null || fileData.length() == 0) {
// nice to put it in the log in case any questions come up.
log.info(user + " POSTing empty source submission on behalf of team " + team_id);

// return Response.status(Response.Status.BAD_REQUEST).entity("no file data specified for " + fileName).build();
}
Comment on lines +633 to +638
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #1173 (handle zero-length files in WTI) includes support for PC2 Admin configuration of "Allow zero-length files?". Once that PR is approved and merged, the above block of code should probably check the ContestInformation configuration item to determine whether to allow a zero-length file. (Unless of course the above block of code actually gets deprecated and eliminated, as suggested above...)

IFile iFile = new IFileImpl(file.getFilename(), fileData);
srcFiles.add(iFile);
}
// allow contestant submission of a zero length file. This will generate a CE (hopefully).
// if the following code is uncommented, the submission is not made and a 400 is returned to the submitter.
// it appears that other CCS's allow zero length submissions. *sigh* -- JB
String fileData = file.getData();
if(fileData == null || fileData.length() == 0) {
// nice to put it in the log in case any questions come up.
log.info(user + " POSTing empty source submission on behalf of team " + team_id);

// return Response.status(Response.Status.BAD_REQUEST).entity("no file data specified for " + fileName).build();
} else {
// There should be precisely one file in the files[] array, which is a zip archive of the source file(s)
if(files.length > 1) {
return Response.status(Response.Status.BAD_REQUEST).entity("only one zip archive is allowed").build();
}
IFile iFile = new IFileImpl(file.getFilename(), fileData);
srcFiles.add(iFile);

srcFiles = EventFeedUtilities.getIFiles(firstFile.getData());
}
String entry = sub.getEntry_point();
IFile mainFile = srcFiles.get(0);
Expand Down
104 changes: 98 additions & 6 deletions src/edu/csus/ecs/pc2/convert/EventFeedUtilities.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
// Copyright (C) 1989-2019 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau.
// Copyright (C) 1989-2025 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau.
package edu.csus.ecs.pc2.convert;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import edu.csus.ecs.pc2.core.model.IFile;
import edu.csus.ecs.pc2.core.model.IFileImpl;

/**
* Event Feed Utilities
*
*
* @author ICPC
*
*/
public final class EventFeedUtilities {

public static final long MS_PER_SECOND = 1000;

private EventFeedUtilities() {
super();
}
Expand All @@ -29,7 +37,7 @@ public static String[] getAllLanguages(List<EventFeedRun> runs) {
map.put(eventFeedRun.getLanguage(), "");
}
Set<String> set = map.keySet();
return (String[]) set.toArray(new String[set.size()]);
return set.toArray(new String[set.size()]);
}

public static int getMaxProblem(List<EventFeedRun> runs) {
Expand All @@ -55,7 +63,7 @@ public static int getMaxTeam(List<EventFeedRun> runs) {

/**
* Convert decimal string to ms.
*
*
* @param decimalSeconds
* - declimal second
* @return ms
Expand Down Expand Up @@ -98,7 +106,7 @@ public static long toMS(String decimalSeconds) {

/**
* Fetch list of filenames, full path
*
*
* @param dirname
* location of submission files
* @param runId
Expand All @@ -123,4 +131,88 @@ public static List<String> fetchRunFileNames(String dirname, String runId) {
return list;
}

/**
* Get files from a zipfile's base64 encoded string data
*
* @param base64Data String comprising a zip file encoded as base64
* @return list of IFiles extracted from the input bytes
* @throws IllegalArgumentException from the base64 decoder on a data error
*/
public static List<IFile> getIFiles(String base64Data) {

// use the decoder to both check the validity of, and to store, the byte data.
// this will throw an IllegalArgumentException if the data is basd
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment? ("basd"?)

return(getIFiles(Base64.getDecoder().decode(base64Data)));
}

/**
* Get files from a zipfile's bytes.
*
* @param bytes bytes comprising a zip file.
* @return list of IFiles extracted from the input bytes
*/
public static List<IFile> getIFiles(byte[] bytes) {

List<IFile> files = new ArrayList<IFile>();

ZipInputStream zipStream = null;

try {
zipStream = new ZipInputStream(new ByteArrayInputStream(bytes));
ZipEntry entry = null;
/**
* Read each zip entry, add IFile.
*/
while ((entry = zipStream.getNextEntry()) != null) {

String entryName = entry.getName();

// ByteOutputStream byteOutputStream = new ByteOutputStream();
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();

byte[] buffer = new byte[8096];
int bytesRead = 0;
while ((bytesRead = zipStream.read(buffer)) != -1)
{
byteOutputStream.write(buffer, 0, bytesRead);
}

// String base64Data = getBase64Data(byteOutputStream.getBytes());
String base64Data = getBase64Data(byteOutputStream.toByteArray());
IFile iFile = new IFileImpl(entryName, base64Data);
files.add(iFile);

byteOutputStream.close();

zipStream.closeEntry();
}
zipStream.close();

} catch (Exception e) {
if (zipStream != null){
try {
zipStream.close();
} catch (Exception ze) {
; // problem closing stream, ignore.
}
}
throw new RuntimeException(e);
}

return files;

}

/**
* Encode bytes into BASE64.
* @param data
* @return
*/
public static String getBase64Data( byte [] bytes) {
// TODO REFACTOR move to FileUtilities
Base64.Encoder encoder = Base64.getEncoder();
String base64String = encoder.encodeToString(bytes);
return base64String;
}

}
Loading