Skip to content

Commit

Permalink
Improved the text preview so it pages correctly. (Velocidex#3324)
Browse files Browse the repository at this point in the history
  • Loading branch information
scudette authored Mar 4, 2024
1 parent d6e1119 commit 23fb6fb
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 82 deletions.
88 changes: 88 additions & 0 deletions api/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ type vfsFileDownloadRequest struct {

// If set we pad the file out.
Padding bool `schema:"padding"`

// If set we filter binary chars to reveal only text
TextFilter bool `schema:"text_filter"`
Lines int `schema:"lines"`
}

// URL format: /api/v1/DownloadVFSFile
Expand Down Expand Up @@ -204,6 +208,25 @@ func vfsFileDownloadHandler() http.Handler {
total_size = calculateTotalReaderSize(file)
}

if request.TextFilter {
output, next_offset, err := filterData(reader_at, request)
if err != nil {
returnError(w, 500, err.Error())
return
}

w.Header().Set("Content-Disposition", "attachment; "+
sanitizeFilenameForAttachment(filename))
w.Header().Set("Content-Type",
detectMime(output, request.DetectMime))
w.Header().Set("Content-Range",
fmt.Sprintf("bytes %d-%d/%d", request.Offset, next_offset, total_size))
w.WriteHeader(200)

_, _ = w.Write(output)
return
}

emitContentLength(w, int(request.Offset), int(request.Length), total_size)

offset := request.Offset
Expand Down Expand Up @@ -265,6 +288,71 @@ func vfsFileDownloadHandler() http.Handler {
})
}

// Read data from offset and filter it until the requested number of
// lines is found. This produces text only output, aka "strings"
func filterData(reader_at io.ReaderAt,
request vfsFileDownloadRequest) (
output []byte, next_offset int64, err error) {

lines := 0
required_lines := request.Lines
if required_lines == 0 {
required_lines = 25
}
offset := request.Offset

buf := pool.Get().([]byte)
defer pool.Put(buf)

// This is a safety mechanism in case the file is mostly 0
total_read := 0

for {
if total_read > 10*1024*1024 {
break
}

n, err := reader_at.ReadAt(buf, offset)
if err != nil && err != io.EOF {
return nil, 0, err
}

if n <= 0 {
break
}

total_read += n

// Read the buffer and filter it collecting only printable
// chars.
for i := 0; i < n; i++ {
c := buf[i]
switch c {
case 0:
continue

case '\n':
lines++
if request.Lines <= lines {
return output, offset + int64(i), nil
}
fallthrough

default:
if c >= 0x20 && c < 0x7f ||
c == 10 || c == 13 || c == 9 {
output = append(output, c)
} else {
output = append(output, '.')
}
}
}
offset += int64(n)
}

return output, offset, nil
}

func detectMime(buffer []byte, detect_mime bool) string {
if detect_mime && len(buffer) > 8 {
if 0 == bytes.Compare(
Expand Down
2 changes: 1 addition & 1 deletion gui/velociraptor/src/components/core/api-service.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ const get_blob = function(url, params, cancel_token) {
var reader = new FileReader();

reader.onloadend = function() {
resolve(reader.result);
resolve({data: reader.result, blob: blob});
};

reader.readAsArrayBuffer(blob.data);
Expand Down
68 changes: 68 additions & 0 deletions gui/velociraptor/src/components/widgets/pagination.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,74 @@ import T from '../i8n/i8n.jsx';
import classNames from "classnames";


export class TextPaginationControl extends React.Component {
static propTypes = {
base_offset: PropTypes.number,
setBaseOffset: PropTypes.func,
}

state = {
next_offset: 0,
}

render() {
return (
<Pagination className="hex-goto">
<Pagination.First
disabled={this.props.current_page===0}
onClick={()=>this.gotoPage(0)}/>
<Pagination.Prev
disabled={this.props.current_page===0}
onClick={()=>this.gotoPage(this.props.current_page-1)}/>

<Form.Control
as="input"
className={classNames({
"page-link": true,
"goto-invalid": this.state.goto_error,
})}
placeholder={T("Goto Offset")}
spellCheck="false"
value={this.state.goto_offset}
onChange={e=> {
let goto_offset = e.target.value;
this.setState({goto_offset: goto_offset});

if (goto_offset === "") {
return;
}

let base_offset = parseInt(goto_offset);
if (isNaN(base_offset)) {
this.setState({goto_error: true});
return;
}
this.setState({goto_error: false});

if (base_offset > this.props.total_size) {
goto_offset = this.props.total_size;
base_offset = this.props.total_size;
this.setState({
goto_offset: goto_offset,
});
}
this.setHighlight(base_offset);

let page = parseInt(base_offset/this.props.page_size);
this.props.onPageChange(page);
}}/>
<Pagination.Next
disabled={last_page == 0 || this.props.current_page===last_page}
onClick={()=>this.gotoPage(this.props.current_page+1)}/>
<Pagination.Last
disabled={last_page == 0 || this.props.current_page===last_page}
onClick={()=>this.gotoPage(last_page)}/>
</Pagination>
);
}
}


export default class HexPaginationControl extends React.Component {
static propTypes = {
total_pages: PropTypes.number,
Expand Down
9 changes: 9 additions & 0 deletions gui/velociraptor/src/components/widgets/preview_uploads.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,12 @@ input.goto-invalid.form-control:focus {
.preview-json {
margin-top: 45px;
}

.text-paginator {
text-wrap: nowrap;
}

.textdump {
overflow-y: auto;
height: calc(100vh - 360px);
}
Loading

0 comments on commit 23fb6fb

Please sign in to comment.