package com.laytonsmith.core.functions;
import com.laytonsmith.PureUtilities.Common.FileUtil;
import com.laytonsmith.PureUtilities.Common.StreamUtils;
import com.laytonsmith.PureUtilities.RunnableQueue;
import com.laytonsmith.PureUtilities.SSHWrapper;
import com.laytonsmith.PureUtilities.Version;
import com.laytonsmith.PureUtilities.ZipReader;
import com.laytonsmith.abstraction.StaticLayer;
import com.laytonsmith.annotations.api;
import com.laytonsmith.annotations.core;
import com.laytonsmith.annotations.noboilerplate;
import com.laytonsmith.core.CHLog;
import com.laytonsmith.core.CHVersion;
import com.laytonsmith.core.LogLevel;
import com.laytonsmith.core.ObjectGenerator;
import com.laytonsmith.core.Optimizable;
import com.laytonsmith.core.ParseTree;
import com.laytonsmith.core.Profiles;
import com.laytonsmith.core.Security;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.constructs.CByteArray;
import com.laytonsmith.core.constructs.CClosure;
import com.laytonsmith.core.constructs.CInt;
import com.laytonsmith.core.constructs.CNull;
import com.laytonsmith.core.constructs.CString;
import com.laytonsmith.core.constructs.CVoid;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.environments.Environment;
import com.laytonsmith.core.environments.GlobalEnv;
import com.laytonsmith.core.exceptions.CRE.CRECastException;
import com.laytonsmith.core.exceptions.CRE.CREIOException;
import com.laytonsmith.core.exceptions.CRE.CRESecurityException;
import com.laytonsmith.core.exceptions.CRE.CREThrowable;
import com.laytonsmith.core.exceptions.CancelCommandException;
import com.laytonsmith.core.exceptions.ConfigCompileException;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
import com.laytonsmith.persistence.DataSourceException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.zip.GZIPInputStream;
/**
*
*/
@core
public class FileHandling {
public static String docs(){
return "This class contains methods that help manage files on the file system. Most are restricted with the base-dir setting"
+ " in your preferences.";
}
@api
@noboilerplate
public static class read extends AbstractFunction {
public static String file_get_contents(String file_location) throws Exception {
return new ZipReader(new File(file_location)).getFileContents();
}
@Override
public String getName() {
return "read";
}
@Override
public Integer[] numArgs() {
return new Integer[]{1};
}
@Override
public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException {
File location = Static.GetFileFromArgument(args[0].val(), env, t, null);
if(!Static.InCmdLine(env)){
//Verify this file is not above the craftbukkit directory (or whatever directory the user specified
//Cmdline mode doesn't currently have this restriction.
if (!Security.CheckSecurity(location)) {
throw new CRESecurityException("You do not have permission to access the file '" + location + "'", t);
}
}
try {
String s = file_get_contents(location.getAbsolutePath());
s = s.replaceAll("\n|\r\n", "\n");
return new CString(s, t);
} catch (Exception ex) {
CHLog.GetLogger().Log(CHLog.Tags.GENERAL, LogLevel.INFO, "Could not read in file while attempting to find "
+ location.getAbsolutePath()
+ "\nFile " + (location.exists() ? "exists" : "does not exist"), t);
throw new CREIOException("File could not be read in.", t);
}
}
@Override
public String docs() {
return "string {file} Reads in a file from the file system at location var1 and returns it as a string. The path is relative to"
+ " the file that is being run, not CommandHelper. If the file is not found, or otherwise can't be read in, an IOException is thrown."
+ " If the file specified is not within base-dir (as specified in the preferences file), a SecurityException is thrown."
+ " The line endings for the string returned will always be \\n, even if they originally were \\r\\n.";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREIOException.class, CRESecurityException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public CHVersion since() {
return CHVersion.V3_0_1;
}
@Override
public Boolean runAsync() {
//Because we do disk IO
return true;
}
@Override
public LogLevel profileAt() {
return LogLevel.DEBUG;
}
}
@api
@noboilerplate
public static class comp_read extends AbstractFunction implements Optimizable {
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
return CNull.NULL;
}
@Override
public String getName() {
return "comp_read";
}
@Override
public Integer[] numArgs() {
return new Integer[]{1};
}
@Override
public String docs() {
return "string {path} Returns the value of a file at compile time only. Unlike read, this runs and is fully resolved"
+ " at compile time. This is useful for optimization reasons, if you have a file that is unchanging, this can be"
+ " used instead of read(), to prevent a runtime hit each time the code is executed. Otherwise, this method is"
+ " equivalent to read(). The path must be fully resolved at compile time.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
@Override
public Set<OptimizationOption> optimizationOptions() {
return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC);
}
@Override
public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException {
if(children.get(0).isDynamic()){
throw new ConfigCompileException(getName() + " can only accept hardcoded paths.", t);
}
Environment env;
try {
env = Static.GenerateStandaloneEnvironment();
} catch (IOException | DataSourceException | URISyntaxException | Profiles.InvalidProfileException ex) {
throw new ConfigCompileException(ex.getMessage(), t, ex);
}
String ret = new read().exec(t, env, children.get(0).getData()).val();
ParseTree tree = new ParseTree(new CString(ret, t), fileOptions);
return tree;
}
}
@api
@noboilerplate
public static class async_read extends AbstractFunction{
RunnableQueue queue = new RunnableQueue("MethodScript-asyncRead");
boolean started = false;
private void startup(){
if(!started){
queue.invokeLater(null, new Runnable() {
@Override
public void run() {
//This warms up the queue. Apparently.
}
});
StaticLayer.GetConvertor().addShutdownHook(new Runnable() {
@Override
public void run() {
queue.shutdown();
started = false;
}
});
started = true;
}
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CRESecurityException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(final Target t, final Environment environment, Construct... args) throws ConfigRuntimeException {
startup();
final String file = args[0].val();
final CClosure callback;
if(!(args[1] instanceof CClosure)){
throw new CRECastException("Expected paramter 2 of " + getName() + " to be a closure!", t);
} else {
callback = ((CClosure)args[1]);
}
if(!Static.InCmdLine(environment)){
if(!Security.CheckSecurity(file)){
throw new CRESecurityException("You do not have permission to access the file '" + file + "'", t);
}
}
queue.invokeLater(environment.getEnv(GlobalEnv.class).GetDaemonManager(), new Runnable() {
@Override
public void run() {
String returnString = null;
ConfigRuntimeException exception = null;
if(file.contains("@")){
try {
//It's an SCP transfer
returnString = SSHWrapper.SCPReadString(file);
} catch (IOException ex) {
exception = new CREIOException(ex.getMessage(), t, ex);
}
} else {
try {
//It's a local file read
File _file = Static.GetFileFromArgument(file, environment, t, null);
returnString = FileUtil.read(_file);
} catch (IOException ex) {
exception = new CREIOException(ex.getMessage(), t, ex);
}
}
final Construct cret;
if(returnString == null){
cret = CNull.NULL;
} else {
cret = new CString(returnString, t);
}
final Construct cex;
if(exception == null){
cex = CNull.NULL;
} else {
cex = ObjectGenerator.GetGenerator().exception(exception, environment, t);
}
StaticLayer.GetConvertor().runOnMainThreadLater(environment.getEnv(GlobalEnv.class).GetDaemonManager(), new Runnable() {
@Override
public void run() {
callback.execute(new Construct[]{cret, cex});
}
});
}
});
return CVoid.VOID;
}
@Override
public String getName() {
return "async_read";
}
@Override
public Integer[] numArgs() {
return new Integer[]{2};
}
@Override
public String docs() {
return "void {file, callback} Asyncronously reads in a file. ---- "
+ " This may be a remote file accessed with an SCP style path. (See the [[CommandHelper/SCP|wiki article]]"
+ " about SCP credentials for more information.) If the file is not found, or otherwise can't be read in, an IOException is thrown."
+ " If the file specified is not within base-dir (as specified in the preferences file), a SecurityException is thrown."
+ " (This is not applicable for remote files)"
+ " The line endings for the string returned will always be \\n, even if they originally were \\r\\n."
+ " This method will immediately return, and asynchronously read in the file, and finally send the contents"
+ " to the callback once the task completes. The callback should have the following signature: closure(@contents, @exception){ <code> }."
+ " If @contents is null, that indicates that an exception occured, and @exception will not be null, but instead have an"
+ " exeption array. Otherwise, @contents will contain the file's contents, and @exception will be null. This method is useful"
+ " to use in two cases, either you need a remote file via SCP, or a local file is big enough that you notice a delay when"
+ " simply using the read() function.";
}
@Override
public CHVersion since() {
return CHVersion.V3_3_1;
}
}
@api
public static class file_size extends AbstractFunction {
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREIOException.class, CRESecurityException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
File location = Static.GetFileFromArgument(args[0].val(), environment, t, null);
if(!Security.CheckSecurity(location) && !Static.InCmdLine(environment)){
throw new CRESecurityException("You do not have permission to access the file '" + location + "'", t);
}
return new CInt(location.length(), t);
}
@Override
public String getName() {
return "file_size";
}
@Override
public Integer[] numArgs() {
return new Integer[]{1};
}
@Override
public String docs() {
return "int {path} Returns the size of a file on the file system.";
}
@Override
public CHVersion since() {
return CHVersion.V3_3_1;
}
}
@api
public static class read_gzip_binary extends AbstractFunction {
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREIOException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException {
File location = Static.GetFileFromArgument(args[0].val(), env, t, null);
if(!Static.InCmdLine(env)){
//Verify this file is not above the craftbukkit directory (or whatever directory the user specified
//Cmdline mode doesn't currently have this restriction.
if (!Security.CheckSecurity(location)) {
throw new CRESecurityException("You do not have permission to access the file '" + location + "'", t);
}
}
try {
InputStream stream = new GZIPInputStream(new FileInputStream(location));
return CByteArray.wrap(StreamUtils.GetBytes(stream), t);
} catch (IOException ex) {
Static.getLogger().log(Level.SEVERE, "Could not read in file while attempting to find " + location.getAbsolutePath()
+ "\nFile " + (location.exists() ? "exists" : "does not exist"));
throw new CREIOException("File could not be read in.", t);
}
}
@Override
public String getName() {
return "read_gzip_binary";
}
@Override
public Integer[] numArgs() {
return new Integer[]{1};
}
@Override
public String docs() {
return "byte_array {file} Reads in a gzipped file, and returns a byte_array for it. The file is returned"
+ " exactly as is on disk, no conversions are done. base-dir restrictions are enforced for the"
+ " path, the same as read(). If file is relative, it is assumed to be relative to this file.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
}
@api
public static class read_binary extends AbstractFunction {
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREIOException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException {
File location = Static.GetFileFromArgument(args[0].val(), env, t, null);
if(!Static.InCmdLine(env)){
//Verify this file is not above the craftbukkit directory (or whatever directory the user specified
//Cmdline mode doesn't currently have this restriction.
if (!Security.CheckSecurity(location)) {
throw new CRESecurityException("You do not have permission to access the file '" + location + "'", t);
}
}
try {
InputStream stream = new BufferedInputStream(new FileInputStream(location));
return CByteArray.wrap(StreamUtils.GetBytes(stream), t);
} catch (IOException ex) {
Static.getLogger().log(Level.SEVERE, "Could not read in file while attempting to find " + location.getAbsolutePath()
+ "\nFile " + (location.exists() ? "exists" : "does not exist"));
throw new CREIOException("File could not be read in.", t);
}
}
@Override
public String getName() {
return "read_binary";
}
@Override
public Integer[] numArgs() {
return new Integer[]{1};
}
@Override
public String docs() {
return "byte_array {file} Reads in a file, and returns a byte_array for it. The file is returned"
+ " exactly as is on disk, no conversions are done. base-dir restrictions are enforced for the"
+ " path, the same as read(). If file is relative, it is assumed to be relative to this file."
+ " This is useful for managing binary files.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
}
//@api
public static class file_parent extends AbstractFunction {
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{};
}
@Override
public boolean isRestricted() {
return false;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
//TODO: Doesn't work yet.
//TODO: Be sure to change over to Static.GetFileFromArgument
String path = args[0].val().trim().replace('\\', '/');
//Remove duplicate /
path = path.replaceAll("(/)(?=.*?/)", path);
if("/".equals(path) || path.matches("[a-zA-Z]:/")){
//This is the root path, return null.
return CNull.NULL;
}
//If the path ends with /, take it off
while(path.endsWith("/")){
path = path.substring(0, path.length() - 2);
}
return new CString(path.substring(0, path.length() - path.lastIndexOf("/")), t);
}
@Override
public String getName() {
return "file_parent";
}
@Override
public Integer[] numArgs() {
return new Integer[]{1};
}
@Override
public String docs() {
return "string {string} Returns the parent directory for the specified file/directory path. For instance,"
+ " given the string '/path/to/file', then '/path/to/' would be returned. Regardless of whether"
+ " or not the system uses forwards or backwards slashes, the file path returned will use forward"
+ " slashes. The path doesn't need to actually exist for this function to work, and the path returned"
+ " is assumed to be a directory, so will always end with '/'. If the path represents the root path,"
+ " for instance, 'C:/' or '/', null is returned.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
}
@api
public static class file_resolve extends AbstractFunction {
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREIOException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
File f = Static.GetFileFromArgument(args[0].val(), environment, t, null);
try {
return new CString(f.getCanonicalPath(), t);
} catch (IOException ex) {
throw new CREIOException(ex.getMessage(), t, ex);
}
}
@Override
public String getName() {
return "file_resolve";
}
@Override
public Integer[] numArgs() {
return new Integer[]{1};
}
@Override
public String docs() {
return "string {file} Returns the canonical, absolute path of the given path. This provides a context independent"
+ " and unique path which always points to the specified path, and removes any duplicate . or .. parts.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
}
}