Skip to content

Commit

Permalink
Allow exporting hash output to CSV or JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
pgp committed Mar 16, 2020
1 parent 3c4dff2 commit b79e06c
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 48 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ android {
minSdkVersion 19
targetSdkVersion 28
multiDexEnabled true
versionCode 153200308
versionName "1.5.3"
versionCode 154200316
versionName "1.5.4"
externalNativeBuild {
cmake {
cppFlags "-std=c++11 -pthread -frtti -fexceptions"
Expand Down
172 changes: 154 additions & 18 deletions app/src/main/java/it/pgp/xfiles/dialogs/ChecksumActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@
import android.widget.TableRow;
import android.widget.Toast;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import it.pgp.xfiles.BrowserItem;
Expand All @@ -33,14 +42,15 @@
import it.pgp.xfiles.R;
import it.pgp.xfiles.adapters.HashAlgorithmsAdapter;
import it.pgp.xfiles.roothelperclient.HashRequestCodes;
import it.pgp.xfiles.utils.FileSaveFragment;
import it.pgp.xfiles.utils.Misc;
import it.pgp.xfiles.utils.pathcontent.BasePathContent;

/**
* Created by pgp on 21/02/18
*/

public class ChecksumActivity extends EffectActivity {
public class ChecksumActivity extends EffectActivity implements FileSaveFragment.Callbacks {

private ChecksumTask checksumTask;

Expand All @@ -49,7 +59,7 @@ public class ChecksumActivity extends EffectActivity {

private HashAlgorithmsAdapter adapter;
private TableLayout standardResultsLayout;
private Button computeChecksumsButton;
private Button computeChecksumsButton, exportChecksumsCSVButton, exportChecksumsJSONButton;

private ClipboardManager clipboard;

Expand All @@ -60,6 +70,9 @@ public class ChecksumActivity extends EffectActivity {
private CheckBox dirHashIgnoreUnixHiddenFiles;
private CheckBox dirHashIgnoreEmptyDirs;

private List<List<HashTextView>> hashMatrix = new ArrayList<>();
private Set<HashRequestCodes> selectedHashAlgorithms; // selected from the last run (not necessarily completed)

public void showLegend(View unused) {
Dialog hashLegendDialog = new Dialog(this){
@Override
Expand Down Expand Up @@ -132,10 +145,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
hashSelectorView.setAdapter(adapter);

computeChecksumsButton = findViewById(R.id.computeChecksumsButton);
computeChecksumsButton.setOnClickListener(v -> {
checksumTask = new ChecksumTask();
checksumTask.execute((Void[])null);
});
exportChecksumsCSVButton = findViewById(R.id.exportChecksumsCSVButton);
exportChecksumsJSONButton = findViewById(R.id.exportChecksumsJSONButton);

// check SHA-256 by default
getViewByPosition(HashRequestCodes.sha256.ordinal(),hashSelectorView).findViewById(R.id.checksum_checkbox).performClick();
Expand Down Expand Up @@ -176,22 +187,62 @@ public boolean onContextItemSelected(MenuItem item) {
return true;
}

private class HashTextView extends android.support.v7.widget.AppCompatTextView {
// FIXME duplicated from CompressActivity, refactor
@Override
public boolean onCanSave(String absolutePath, String fileName) {

// Catch the really stupid case.
if (absolutePath == null || absolutePath.length() ==0 ||
fileName == null || fileName.length() == 0) {
Toast.makeText(this,R.string.alert_supply_filename, Toast.LENGTH_SHORT).show();
return false;
}

// Do we have a filename if the extension is thrown away?
String copyName = FileSaveFragment.NameNoExtension(fileName);
if (copyName == null || copyName.length() == 0 ) {
Toast.makeText(this,R.string.alert_supply_filename, Toast.LENGTH_SHORT).show();
return false;
}

// No overwrite of an existing file.
if (FileSaveFragment.FileExists(absolutePath, fileName)) {
Toast.makeText(this,R.string.alert_file_exists, Toast.LENGTH_SHORT).show();
return false;
}

return true;
}

@Override
public void onConfirmSave(String absolutePath, String fileName) {
if (onCanSave(absolutePath,fileName)) {
// export CSV only for now
if(fileName.endsWith("csv"))
exportToCSV(absolutePath,fileName);
else exportToJSON(absolutePath,fileName);
}
}

private static class HashTextView extends android.support.v7.widget.AppCompatTextView {

private CharSequence header;
private CharSequence content;
private HashRequestCodes code;
private CharSequence filename;
private CharSequence content; // the digest as hex string

public HashTextView(Context context,
CharSequence content,
CharSequence header) {
CharSequence filename,
HashRequestCodes code) {
super(context);
this.content = content;
this.header = header;
setText(header+": "+content);
this.code = code;
this.filename = filename;
setText(getHeader()+": "+content);
}

public CharSequence getHeader() {
return header;
return filename+", "+code.getLabel();
}

public CharSequence getContent() {
Expand All @@ -209,8 +260,12 @@ protected void onDestroy() {
}

private class ChecksumTask extends AsyncTask<Void, Void, Void> {
final Set<HashRequestCodes> selectedHashAlgorithms = adapter.getSelectedItems();
final boolean someSelection = selectedHashAlgorithms.size()>0;
final boolean someSelection;

{
selectedHashAlgorithms = adapter.getSelectedItems();
someSelection = selectedHashAlgorithms.size()>0;
}

@Override
protected void onPreExecute() {
Expand All @@ -235,14 +290,17 @@ private byte[] computeHashForLocalOrXREPaths(BasePathContent path, HashRequestCo

@Override
protected Void doInBackground(Void... unused) {
hashMatrix = new ArrayList<>();
final int[][] tvBackground = new int[][]{{Color.DKGRAY,Color.BLUE},{Color.RED,Color.GRAY}};

// TODO restructure hash request in RH protocol, allow multiple hashes per multiple files
// then, make the RH task cancellable via another sub-request type (like in FindUpdatesThread)
int i = 0,j=0;
int i=0, j=0;
try {
if(!someSelection) return null;
if (files.size()==1) { // algorithms on rows, 1 column (only 1 file)
List<HashTextView> lhtv = new ArrayList<>(); // for csv/json export, keep the format coherent (1 row, multiple columns)
hashMatrix.add(lhtv);
BasePathContent file = parentDir.concat(files.get(0).getFilename());
for (HashRequestCodes s : selectedHashAlgorithms) {
if (checksumInterrupted) {
Expand All @@ -258,15 +316,18 @@ protected Void doInBackground(Void... unused) {
HashTextView t = new HashTextView(
ChecksumActivity.this,
Misc.toHexString(digest),
file.getName()+", "+s.getLabel() );
file.getName(), s);

t.setBackgroundColor(tvBackground[i][j++%2]);
registerForContextMenu(t);
lhtv.add(t);
runOnUiThread(()->tr.addView(t));
}
}
else for (BrowserItem file : files) { // files on rows
TableRow tr = new TableRow(ChecksumActivity.this);
List<HashTextView> lhtv = new ArrayList<>();
hashMatrix.add(lhtv);
runOnUiThread(()->standardResultsLayout.addView(tr));

for (HashRequestCodes s : selectedHashAlgorithms) {
Expand All @@ -281,10 +342,11 @@ else for (BrowserItem file : files) { // files on rows
HashTextView t = new HashTextView(
ChecksumActivity.this,
Misc.toHexString(digest),
file.getFilename()+", "+s.getLabel() );
file.getFilename(), s);

t.setBackgroundColor(tvBackground[i][j++%2]);
registerForContextMenu(t);
lhtv.add(t);
runOnUiThread(()->tr.addView(t));

}
Expand All @@ -306,4 +368,78 @@ protected void onPostExecute(Void unused) {
computeChecksumsButton.setEnabled(true);
}
}

public void ok(View unused) {
checksumTask = new ChecksumTask();
checksumTask.execute((Void[])null);
}

public void openExportOutputSelector(View v) {
if(hashMatrix.isEmpty()) {
Toast.makeText(this, "No checksums to export", Toast.LENGTH_SHORT).show();
return;
}

String fragTag = getResources().getString(R.string.tag_fragment_FileSave);

String ext = v.getId()==R.id.exportChecksumsCSVButton?"csv":"json";

// Get an instance supplying a default extension, captions and
// icon appropriate to the calling application/activity.
FileSaveFragment fsf = FileSaveFragment.newInstance(ext,
R.string.alert_OK,
R.string.alert_cancel,
R.string.app_name,
R.string.edit_hint,
R.string.checksums_export_filename_header,
R.drawable.xfiles_file_icon);
fsf.show(getFragmentManager(), fragTag);
}

public void exportToCSV(String absolutePath, String fileName) {
File csvFile = new File(absolutePath +"/"+fileName+".csv");
try(OutputStream o = new BufferedOutputStream(new FileOutputStream(csvFile))) {
// create header
Misc.csvWriteRow(o,new ArrayList(){{
add("filename");
for (HashRequestCodes code : selectedHashAlgorithms) add(code.getLabel());
}});

for(List<HashTextView> lhtv : hashMatrix) {
Misc.csvWriteRow(o,new ArrayList(){{
// by construction, lhtv contains hashes for the same filename
add(""+lhtv.get(0).filename);
for(HashTextView htv: lhtv) add(""+htv.content);
}});
}

MainActivity.showToastOnUI("Checksums export complete");
} catch (IOException e) {
e.printStackTrace();
MainActivity.showToastOnUI("Error exporting checksums to CSV");
}
}

public void exportToJSON(String absolutePath, String fileName) {
List l = new ArrayList();
for(List<HashTextView> lhtv : hashMatrix) {
Map m = new HashMap();
Map n = new HashMap();
for(HashTextView htv : lhtv) {
n.put(htv.code.getLabel(),""+htv.content);
}
m.put("filename",""+lhtv.get(0).filename);
m.put("checksums",n);
l.add(m);
}
ObjectMapper mapper = new ObjectMapper();
try {
mapper.writeValue(new File(absolutePath+"/"+fileName+".json"),l);
MainActivity.showToastOnUI("Checksums export complete");
}
catch (IOException e) {
e.printStackTrace();
MainActivity.showToastOnUI("Error exporting checksums to JSON");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,24 +177,6 @@ public void updateHostKey(String adjustedHostname, PublicKey key) throws IOExcep

/************************************************************/

// replaced by RemotePathContent
// private class GenericRemotePath {
// AuthData authData; // parsed from sftp://user@domain:port
// String remotePath; // format: /remote/path
//
// GenericRemotePath(String path) throws RuntimeException {
// if (path.startsWith("sftp://")) {
// String s = path.substring(7); // user@domain:port/remote/path
// String authString = s.split("/")[0]; // user@domain:port
// authData = new AuthData(authString);
// int idx = s.indexOf('/');
// // remote path beginning immediately after port number, and beginning with '/'
// remotePath = s.substring(idx);
// }
// else throw new RuntimeException("");
// }
// }

public SFTPProviderUsingPathContent(final MainActivity mainActivity) {
this.mainActivity = mainActivity;
sshIdsDir = new File(mainActivity.getApplicationContext().getFilesDir(),sshIdsDirName);
Expand Down
7 changes: 2 additions & 5 deletions app/src/main/java/it/pgp/xfiles/utils/FileSaveFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,8 @@
import it.pgp.xfiles.utils.dircontent.GenericDirWithContent;
import it.pgp.xfiles.utils.pathcontent.LocalPathContent;

/** Allow user to select destination directory and to enter filename.
*
* */
public class FileSaveFragment extends DialogFragment
implements OnItemClickListener {
/** Allow user to select destination directory and to enter filename */
public class FileSaveFragment extends DialogFragment implements OnItemClickListener {

/*
* Use the unicode "back" triangle to indicate there is a parent
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/it/pgp/xfiles/utils/HashView.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ void draw2(Canvas canvas, PaintRect[][] M) {
canvas.drawRect(anAM.rect, anAM.rPaint);
}

private class PaintRect {
private static class PaintRect {
Rect rect;
Paint rPaint;

Expand Down
30 changes: 30 additions & 0 deletions app/src/main/java/it/pgp/xfiles/utils/Misc.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -197,4 +198,33 @@ public static List<String> splitByteArrayOverByteAndEncode(byte[] b, byte target
return outs;
}

// CSV escape (for checksum export)
public static final String[] csvToBeEscaped = {"\"",",","\n"};

public static final byte[] crlf = new byte[]{'\r','\n'};

/**
* - if filename contains any between { \n " , }, use enclosing quotes
* - if filename contains " escape it as ""
*/
public static String escapeForCSV(String filename) {
boolean enclosingQuotes = false;
for(String x : csvToBeEscaped)
if(filename.contains(x)) {
enclosingQuotes = true;
break;
}

return enclosingQuotes?"\""+filename.replace("\"","\"\"")+"\"":filename;
}

public static void csvWriteRow(OutputStream o, List<String> row) throws IOException {
for(int i=0;i<row.size()-1;i++)
o.write((escapeForCSV(row.get(i))+",").getBytes(StandardCharsets.UTF_8));

// fine to have IndexOutOfBounds with empty list
o.write(escapeForCSV(row.get(row.size()-1)).getBytes(StandardCharsets.UTF_8));
o.write(crlf);
}

}
2 changes: 1 addition & 1 deletion app/src/main/java/it/pgp/xfiles/utils/VMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public VMap() {
h = new HashMap<>();
}

public class ValueAsKeyException extends RuntimeException {
public static class ValueAsKeyException extends RuntimeException {
public ValueAsKeyException() {
super("Tried to access value as key!");
}
Expand Down
Loading

0 comments on commit b79e06c

Please sign in to comment.