commit
5fcca76d2d
21 changed files with 630 additions and 0 deletions
@ -0,0 +1,11 @@ |
|||
^.*\.Rproj$ |
|||
^\.Rproj\.user$ |
|||
^\.travis\.yml$ |
|||
^README\.*Rmd$ |
|||
^README\.*html$ |
|||
^NOTES\.*Rmd$ |
|||
^NOTES\.*html$ |
|||
^\.codecov\.yml$ |
|||
^README_files$ |
|||
^doc$ |
|||
^tmp$ |
@ -0,0 +1 @@ |
|||
comment: false |
@ -0,0 +1,8 @@ |
|||
.DS_Store |
|||
.Rproj.user |
|||
.Rhistory |
|||
.RData |
|||
.Rproj |
|||
src/*.o |
|||
src/*.so |
|||
src/*.dll |
@ -0,0 +1,6 @@ |
|||
language: R |
|||
sudo: false |
|||
cache: packages |
|||
|
|||
after_success: |
|||
- Rscript -e 'covr::codecov()' |
@ -0,0 +1,26 @@ |
|||
Package: mactheknife |
|||
Type: Package |
|||
Title: Read 'macOS' .DS_Store' Files |
|||
Version: 0.1.0 |
|||
Date: 2018-04-29 |
|||
Authors@R: c( |
|||
person("Bob", "Rudis", email = "bob@rud.is", role = c("aut", "cre"), |
|||
comment = c(ORCID = "0000-0001-5670-2640")), |
|||
person("Sebastian", "Neef", email = "github@gehaxelt.in", role = c("aut", "cph"), |
|||
comment = "Python dsstore module <https://github.com/gehaxelt/Python-dsstore>") |
|||
) |
|||
Maintainer: Bob Rudis <bob@rud.is> |
|||
Description: A thin wrapper around the 'Python' 'dsstore' module |
|||
<https://github.com/gehaxelt/Python-dsstore> by 'Sebastian Neef'. |
|||
URL: https://github.com/hrbrmstr/mactheknife |
|||
BugReports: https://github.com/hrbrmstr/mactheknife/issues |
|||
SystemRequirements: Python |
|||
Encoding: UTF-8 |
|||
License: MIT + file LICENSE |
|||
Suggests: |
|||
testthat, |
|||
covr |
|||
Depends: |
|||
R (>= 3.2.0), |
|||
reticulate |
|||
RoxygenNote: 6.0.1.9000 |
@ -0,0 +1,2 @@ |
|||
YEAR: 2018 |
|||
COPYRIGHT HOLDER: Bob Rudis |
@ -0,0 +1,4 @@ |
|||
# Generated by roxygen2: do not edit by hand |
|||
|
|||
export(read_dsstore) |
|||
import(reticulate) |
@ -0,0 +1,2 @@ |
|||
0.1.0 |
|||
* Initial release |
@ -0,0 +1,11 @@ |
|||
#' Read 'macOS' .DS_Store' Files |
|||
#' |
|||
#' A thin wrapper around the 'Python' 'dsstore' module |
|||
#' <https://github.com/gehaxelt/Python-dsstore> by 'Sebastian Neef'. |
|||
#' |
|||
#' @md |
|||
#' @name mactheknife |
|||
#' @docType package |
|||
#' @author Bob Rudis (bob@@rud.is) |
|||
#' @import reticulate |
|||
NULL |
@ -0,0 +1,29 @@ |
|||
#' Read a `.DS_Store` file |
|||
#' |
|||
#' @md |
|||
#' @param path a path to a valid `.DS_Store` file ([path.expand()] will be called) |
|||
#' @return a character vector of filenames in the `.DS_Store` file or |
|||
#' a length 0 character vector if no parseable data was found |
|||
#' @export |
|||
#' @examples |
|||
#' read_dsstore(system.file("extdat", "DS_Store.ctf", package = "mactheknife")) |
|||
read_dsstore <- function(path) { |
|||
|
|||
stor_path <- path.expand(path) |
|||
stor_path <- normalizePath(stor_path) |
|||
|
|||
fil <- os$open(stor_path, os$O_RDONLY) |
|||
contents <- os$read(fil, as.integer(file.size(stor_path))) |
|||
os$close(fil) |
|||
|
|||
d <- dsstore$DS_Store(contents) |
|||
|
|||
ds_fils <- d$traverse_root() |
|||
|
|||
out <- unique(ds_fils) |
|||
|
|||
if (length(out) == 0) out <- character() |
|||
|
|||
out |
|||
|
|||
} |
@ -0,0 +1,11 @@ |
|||
dsstore <- NULL |
|||
os <- NULL |
|||
|
|||
.onLoad <- function(libname, pkgname) { |
|||
dsstore <<- reticulate::import_from_path( |
|||
module = "dsstore", |
|||
path = system.file("modules", package = "mactheknife"), |
|||
delay_load = TRUE |
|||
) |
|||
os <<- reticulate::import("os", delay_load = TRUE) |
|||
} |
@ -0,0 +1,64 @@ |
|||
--- |
|||
output: rmarkdown::github_document |
|||
--- |
|||
|
|||
# mactheknife |
|||
|
|||
Read 'macOS' .DS_Store' Files |
|||
|
|||
## Description |
|||
|
|||
A thin wrapper around the 'Python' 'dsstore' module <https://github.com/gehaxelt/Python-dsstore> by 'Sebastian Neef'. |
|||
|
|||
## NOTE |
|||
|
|||
- This may turn into a broader "macOS hacking" package |
|||
- Uses `reticulate` so a working Python implementation is needed |
|||
|
|||
## What's Inside The Tin |
|||
|
|||
- `read_dsstore`: Read a '.DS_Store' file |
|||
|
|||
The following functions are implemented: |
|||
|
|||
## Installation |
|||
|
|||
```{r eval=FALSE} |
|||
devtools::install_github("hrbrmstr/mactheknife") |
|||
``` |
|||
|
|||
```{r message=FALSE, warning=FALSE, error=FALSE, include=FALSE} |
|||
options(width=120) |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
```{r message=FALSE, warning=FALSE, error=FALSE} |
|||
library(mactheknife) |
|||
|
|||
# current verison |
|||
packageVersion("mactheknife") |
|||
|
|||
``` |
|||
|
|||
## Built-in data |
|||
|
|||
```{r} |
|||
read_dsstore( |
|||
path = system.file("extdat", "DS_Store.ctf", package = "mactheknife") |
|||
) |
|||
``` |
|||
|
|||
## My "~/projects" folder (use your own dir as an example) |
|||
|
|||
```{r} |
|||
library(magrittr) |
|||
|
|||
list.files( |
|||
path = "~/projects", pattern = "\\.DS_Store", |
|||
all.files=TRUE, recursive = TRUE, full.names = TRUE |
|||
) %>% |
|||
lapply(read_dsstore) -> x |
|||
|
|||
str(x) |
|||
``` |
@ -0,0 +1,84 @@ |
|||
|
|||
# mactheknife |
|||
|
|||
Read ‘macOS’ .DS\_Store’ Files |
|||
|
|||
## Description |
|||
|
|||
A thin wrapper around the ‘Python’ ‘dsstore’ module |
|||
<https://github.com/gehaxelt/Python-dsstore> by ‘Sebastian Neef’. |
|||
|
|||
## NOTE |
|||
|
|||
- This may turn into a broader “macOS hacking” package |
|||
- Uses `reticulate` so a working Python implementation is needed |
|||
|
|||
## What’s Inside The Tin |
|||
|
|||
- `read_dsstore`: Read a ‘.DS\_Store’ file |
|||
|
|||
The following functions are implemented: |
|||
|
|||
## Installation |
|||
|
|||
``` r |
|||
devtools::install_github("hrbrmstr/mactheknife") |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
``` r |
|||
library(mactheknife) |
|||
|
|||
# current verison |
|||
packageVersion("mactheknife") |
|||
``` |
|||
|
|||
## [1] '0.1.0' |
|||
|
|||
## Built-in data |
|||
|
|||
``` r |
|||
read_dsstore( |
|||
path = system.file("extdat", "DS_Store.ctf", package = "mactheknife") |
|||
) |
|||
``` |
|||
|
|||
## [1] "favicon.ico" "flag" "static" "templates" "vulnerable.py" "vulnerable.wsgi" |
|||
|
|||
## My “~/projects” folder (use your own dir as an example) |
|||
|
|||
``` r |
|||
library(magrittr) |
|||
|
|||
list.files( |
|||
path = "~/projects", pattern = "\\.DS_Store", |
|||
all.files=TRUE, recursive = TRUE, full.names = TRUE |
|||
) %>% |
|||
lapply(read_dsstore) -> x |
|||
|
|||
str(x) |
|||
``` |
|||
|
|||
## List of 21 |
|||
## $ : chr [1:20] "2017-dashboard" "2017-tlapd" "cataps" "congress-privacy" ... |
|||
## $ : chr "greenery-palettes" |
|||
## $ : chr "data" |
|||
## $ : chr "data" |
|||
## $ : chr(0) |
|||
## $ : chr(0) |
|||
## $ : chr(0) |
|||
## $ : chr "packrat" |
|||
## $ : chr "lib" |
|||
## $ : chr "x86_64-apple-darwin15.6.0" |
|||
## $ : chr "3.4.0" |
|||
## $ : chr(0) |
|||
## $ : chr "data" |
|||
## $ : chr "lyme" |
|||
## $ : chr "packrat" |
|||
## $ : chr "lib" |
|||
## $ : chr "x86_64-apple-darwin15.6.0" |
|||
## $ : chr "3.4.1" |
|||
## $ : chr "plots" |
|||
## $ : chr [1:2] "top-1m.csv" "top-1m.csv.zip" |
|||
## $ : chr(0) |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,307 @@ |
|||
import struct |
|||
|
|||
class ParsingError(Exception): pass |
|||
|
|||
class DataBlock(object): |
|||
""" |
|||
Class for a basic DataBlock inside of the DS_Store format. |
|||
""" |
|||
def __init__(self, data, debug=False): |
|||
super(DataBlock, self).__init__() |
|||
self.data = data |
|||
self.pos = 0 |
|||
self.debug = debug |
|||
|
|||
def offset_read(self, length, offset=None): |
|||
""" |
|||
Returns an byte array of length from data at the given offset or pos. |
|||
If no offset is given, pos will be increased by length. |
|||
Throws ParsingError if offset+length > len(self.data) |
|||
""" |
|||
if not offset: |
|||
offset_position = self.pos |
|||
else: |
|||
offset_position = offset |
|||
|
|||
if len(self.data) < offset_position+length: |
|||
raise ParsingError("Offset+Length > len(self.data)") |
|||
|
|||
if not offset: |
|||
self.pos += length |
|||
|
|||
value = self.data[offset_position:offset_position+length] |
|||
self._log("Reading: {}-{} => {}".format(hex(offset_position), hex(offset_position+length), value)) |
|||
return value |
|||
|
|||
def skip(self, length): |
|||
""" |
|||
Increases pos by length without reading data! |
|||
""" |
|||
self.pos += length |
|||
|
|||
def read_filename(self): |
|||
""" |
|||
Extracts a file name from the current position. |
|||
""" |
|||
# The length of the file name in bytes. |
|||
length, = struct.unpack_from(">I", self.offset_read(4)) |
|||
# The file name in UTF-16, which is two bytes per character. |
|||
filename = self.offset_read(2 * length).decode("utf-16be") |
|||
# A structure ID that I haven't found any use of. |
|||
structure_id, = struct.unpack_from(">I", self.offset_read(4)) |
|||
# Now read the structure type as a string of four characters and decode it to ascii. |
|||
structure_type, = struct.unpack_from(">4s", self.offset_read(4)) |
|||
|
|||
structure_type = structure_type.decode() |
|||
self._log("Structure type ", structure_type) |
|||
# If we don't find a match, skip stays < 0 and we will do some magic to find the right skip due to somehow broken .DS_Store files.. |
|||
skip = -1 |
|||
# Source: http://search.cpan.org/~wiml/Mac-Finder-DSStore/DSStoreFormat.pod |
|||
while skip < 0: |
|||
if structure_type == "bool": |
|||
skip = 1 |
|||
elif structure_type == "type" or structure_type == "long" or structure_type == "shor" or structure_type == "fwsw" or structure_type == "fwvh" or structure_type == "icvt" or structure_type == "lsvt" or structure_type == "vSrn" or structure_type == "vstl": |
|||
skip = 4 |
|||
elif structure_type == "comp" or structure_type == "dutc" or structure_type == "icgo" or structure_type == "icsp" or structure_type == "logS" or structure_type == "lg1S" or structure_type == "lssp" or structure_type == "modD" or structure_type == "moDD" or structure_type == "phyS" or structure_type == "ph1S": |
|||
skip = 8 |
|||
elif structure_type == "blob": |
|||
blen, = struct.unpack_from(">I", self.offset_read(4)) |
|||
skip = blen |
|||
elif structure_type == "ustr" or structure_type == "cmmt" or structure_type == "extn" or structure_type == "GRP0": |
|||
blen, = struct.unpack_from(">I", self.offset_read(4)) |
|||
skip = 2* blen |
|||
elif structure_type == "BKGD": |
|||
skip = 12 |
|||
elif structure_type == "ICVO" or structure_type == "LSVO" or structure_type == "dscl": |
|||
skip = 1 |
|||
elif structure_type == "Iloc" or structure_type == "fwi0": |
|||
skip = 16 |
|||
elif structure_type == "dilc": |
|||
skip = 32 |
|||
elif structure_type == "lsvo": |
|||
skip = 76 |
|||
elif structure_type == "icvo": |
|||
pass |
|||
elif structure_type == "info": |
|||
pass |
|||
else: |
|||
pass |
|||
|
|||
if skip <= 0: |
|||
# We somehow didn't find a matching type. Maybe this file name's length value is broken. Try to fix it! |
|||
# This is a bit voodoo and probably not the nicest way. Beware, there by dragons! |
|||
self._log("Re-reading!") |
|||
# Rewind 8 bytes, so that we can re-read structure_id and structure_type |
|||
self.skip(-1 * 2 * 0x4) |
|||
filename += self.offset_read(0x2).decode("utf-16be") |
|||
# re-read structure_id and structure_type |
|||
structure_id, = struct.unpack_from(">I", self.offset_read(4)) |
|||
structure_type, = struct.unpack_from(">4s", self.offset_read(4)) |
|||
structure_type = structure_type.decode() |
|||
# Look-ahead and check if we have structure_type==Iloc followed by blob. |
|||
# If so, we're interested in blob, not Iloc. Otherwise continue! |
|||
future_structure_type = struct.unpack_from(">4s", self.offset_read(4, offset=self.pos)) |
|||
self._log("Re-read structure_id {} / structure_type {}".format(structure_id, structure_type)) |
|||
if structure_type != "blob" and future_structure_type != "blob": |
|||
structure_type = "" |
|||
self._log("Forcing another round!") |
|||
|
|||
|
|||
# Skip bytes until the next (file name) block |
|||
self.skip(skip) |
|||
self._log("Filename {}".format(filename)) |
|||
return filename |
|||
|
|||
def _log(self, *args): |
|||
if self.debug: |
|||
print("[DEBUG] ", *args) |
|||
|
|||
class DS_Store(DataBlock, object): |
|||
""" |
|||
Represents the .DS_Store file from the given binary data. |
|||
""" |
|||
def __init__(self, data, debug=False): |
|||
super(DS_Store, self).__init__(data, debug) |
|||
self.data = data |
|||
self.root = self.__read_header() |
|||
self.offsets = self.__read_offsets() |
|||
self.toc = self.__read_TOC() |
|||
self.freeList = self.__read_freelist() |
|||
self.debug = debug |
|||
|
|||
def __read_header(self): |
|||
""" |
|||
Checks if self.data is actually a .DS_Store file by checking the magic bytes. |
|||
It returns the file's root block. |
|||
""" |
|||
# We read at least 32+4 bytes for the header! |
|||
if len(self.data) < 36: |
|||
raise ParsingError("Length of data is too short!") |
|||
|
|||
# Check the magic bytes for .DS_Store |
|||
magic1, magic2 = struct.unpack_from(">II", self.offset_read(2*4)) |
|||
if not magic1 == 0x1 and not magic2 == 0x42756431: |
|||
raise ParsingError("Magic byte 1 does not match!") |
|||
|
|||
# After the magic bytes, the offset follows two times with block's size in between. |
|||
# Both offsets have to match and are the starting point of the root block |
|||
offset, size, offset2 = struct.unpack_from(">III", self.offset_read(3*4)) |
|||
self._log("Offset 1: {}".format(offset)) |
|||
self._log("Size: {}".format(size)) |
|||
self._log("Offset 2: {}".format(offset2)) |
|||
if not offset == offset2: |
|||
raise ParsingError("Offsets do not match!") |
|||
# Skip 16 bytes of unknown data... |
|||
self.skip(4*4) |
|||
|
|||
return DataBlock(self.offset_read(size, offset+4), debug=self.debug) |
|||
|
|||
def __read_offsets(self): |
|||
""" |
|||
Reads the offsets which follow the header. |
|||
""" |
|||
start_pos = self.root.pos |
|||
# First get the number of offsets in this file. |
|||
count, = struct.unpack_from(">I", self.root.offset_read(4)) |
|||
self._log("Offset count: {}".format(count)) |
|||
# Always appears to be zero! |
|||
self.root.skip(4) |
|||
|
|||
# Iterate over the offsets and get the offset addresses. |
|||
offsets = [] |
|||
for i in range(count): |
|||
# Address of the offset. |
|||
address, = struct.unpack_from(">I", self.root.offset_read(4)) |
|||
self._log("Offset {} is {}".format(i, address)) |
|||
if address == 0: |
|||
# We're only interested in non-zero values |
|||
continue |
|||
offsets.append(address) |
|||
|
|||
# Calculate the end of the address space (filled with zeroes) instead of dumbly reading zero values... |
|||
section_end = start_pos + (count // 256 + 1) * 256 * 4 - count*4 |
|||
|
|||
# Skip to the end of the section |
|||
self.root.skip(section_end) |
|||
self._log("Skipped {} to {}".format(hex(self.root.pos + section_end), hex(self.root.pos))) |
|||
self._log("Offsets: {}".format(offsets)) |
|||
return offsets |
|||
|
|||
def __read_TOC(self): |
|||
""" |
|||
Reads the table of contents (TOCs) from the file. |
|||
""" |
|||
self._log("POS {}".format(hex(self.root.pos))) |
|||
# First get the number of ToC entries. |
|||
count, = struct.unpack_from(">I", self.root.offset_read(4)) |
|||
self._log("Toc count: {}".format(count)) |
|||
toc = {} |
|||
# Iterate over all ToCs |
|||
for i in range(count): |
|||
# Get the length of a ToC's name |
|||
toc_len, = struct.unpack_from(">b", self.root.offset_read(1)) |
|||
# Read the ToC's name |
|||
toc_name, = struct.unpack_from(">{}s".format(toc_len), self.root.offset_read(toc_len)) |
|||
# Read the address (block id) in the data section |
|||
block_id, = struct.unpack_from(">I", self.root.offset_read(4)) |
|||
# Add all values to the dictionary |
|||
toc[toc_name.decode()]= block_id |
|||
|
|||
self._log("Toc {}".format(toc)) |
|||
return toc |
|||
|
|||
def __read_freelist(self): |
|||
""" |
|||
Read the free list from the header. |
|||
The free list has n=0..31 buckets with the index 2^n |
|||
""" |
|||
freelist = {} |
|||
for i in range(32): |
|||
freelist[2**i] = [] |
|||
# Read the amount of blocks in the specific free list. |
|||
blkcount, = struct.unpack_from(">I", self.root.offset_read(4)) |
|||
for j in range(blkcount): |
|||
# Read blkcount block offsets. |
|||
free_offset, = struct.unpack_from(">I", self.root.offset_read(4)) |
|||
freelist[2**i].append(free_offset) |
|||
|
|||
self._log("Freelist: {}".format(freelist)) |
|||
return freelist |
|||
|
|||
def __block_by_id(self, block_id): |
|||
""" |
|||
Create a DataBlock from a given block ID (e.g. from the ToC) |
|||
""" |
|||
# First check if the block_id is within the offsets range |
|||
if len(self.offsets) < block_id: |
|||
raise ParsingError("BlockID out of range!") |
|||
|
|||
# Get the address of the block |
|||
addr = self.offsets[block_id] |
|||
|
|||
# Do some necessary bit operations to extract the offset and the size of the block. |
|||
# The address without the last 5 bits is the offset in the file |
|||
offset = (int(addr) >> 0x5 << 0x5) |
|||
# The address' last five bits are the block's size. |
|||
size = 1 << (int(addr) & 0x1f) |
|||
self._log("New block: addr {} offset {} size {}".format( addr, offset + 0x4, size)) |
|||
# Return the new block |
|||
return DataBlock(self.offset_read(size, offset + 0x4), debug=self.debug) |
|||
|
|||
def traverse_root(self): |
|||
""" |
|||
Traverse from the root block and extract all file names. |
|||
""" |
|||
# Get the root block from the ToC 'DSDB' |
|||
root = self.__block_by_id(self.toc['DSDB']) |
|||
# Read the following root block's ID, so that we can traverse it. |
|||
root_id, = struct.unpack(">I", root.offset_read(4)) |
|||
self._log("Root-ID ", root_id) |
|||
|
|||
# Read other values that we might be useful, but we're not interested in... (at least right now) |
|||
internal_block_count, = struct.unpack(">I", root.offset_read(4)) |
|||
record_count, = struct.unpack(">I", root.offset_read(4)) |
|||
block_count, = struct.unpack(">I", root.offset_read(4)) |
|||
unknown, = struct.unpack(">I", root.offset_read(4)) |
|||
|
|||
# traverse from the extracted root block id. |
|||
return self.traverse(root_id) |
|||
|
|||
def traverse(self, block_id): |
|||
""" |
|||
Traverses a block identified by the given block_id and extracts the file names. |
|||
""" |
|||
# Get the responsible block by it's ID |
|||
node = self.__block_by_id(block_id) |
|||
# Extract the pointer to the next block |
|||
next_pointer, = struct.unpack(">I", node.offset_read(4)) |
|||
# Get the number of next blocks or records |
|||
count, = struct.unpack(">I", node.offset_read(4)) |
|||
self._log("Next Ptr {} with {} ".format(hex(next_pointer), hex(count))) |
|||
|
|||
filenames = [] |
|||
# If a next_pointer exists (>0), iterate through the next blocks recursively |
|||
# If not, we extract all file names from the current block |
|||
if next_pointer > 0: |
|||
for i in range(0, count, 1): |
|||
# Get the block_id for the next block |
|||
next_id, = struct.unpack(">I", node.offset_read(4)) |
|||
self._log("Child: {}".format(next_id)) |
|||
# Traverse it recursively |
|||
files = self.traverse(next_id) |
|||
filenames += files |
|||
# Also get the filename for the current block. |
|||
filename = node.read_filename() |
|||
self._log("Filename: ", filename) |
|||
filenames.append(filename) |
|||
# Now that we traversed all childs of the next_pointer, traverse the pointer itself. |
|||
# TODO: Check if that is really necessary as the last child should be the current node... (or so?) |
|||
files = self.traverse(next_pointer) |
|||
filenames += files |
|||
else: |
|||
# We're probably in a leaf node, so extract the file names. |
|||
for i in range(0, count, 1): |
|||
f = node.read_filename() |
|||
filenames.append(f) |
|||
|
|||
return filenames |
@ -0,0 +1,21 @@ |
|||
Version: 1.0 |
|||
|
|||
RestoreWorkspace: Default |
|||
SaveWorkspace: Default |
|||
AlwaysSaveHistory: Default |
|||
|
|||
EnableCodeIndexing: Yes |
|||
UseSpacesForTab: Yes |
|||
NumSpacesForTab: 2 |
|||
Encoding: UTF-8 |
|||
|
|||
RnwWeave: Sweave |
|||
LaTeX: pdfLaTeX |
|||
|
|||
StripTrailingWhitespace: Yes |
|||
|
|||
BuildType: Package |
|||
PackageUseDevtools: Yes |
|||
PackageInstallArgs: --no-multiarch --with-keep.source |
|||
PackageBuildArgs: --resave-data |
|||
PackageRoxygenize: rd,collate,namespace |
@ -0,0 +1,14 @@ |
|||
% Generated by roxygen2: do not edit by hand |
|||
% Please edit documentation in R/mactheknife-package.R |
|||
\docType{package} |
|||
\name{mactheknife} |
|||
\alias{mactheknife} |
|||
\alias{mactheknife-package} |
|||
\title{Read 'macOS' .DS_Store' Files} |
|||
\description{ |
|||
A thin wrapper around the 'Python' 'dsstore' module |
|||
\url{https://github.com/gehaxelt/Python-dsstore} by 'Sebastian Neef'. |
|||
} |
|||
\author{ |
|||
Bob Rudis (bob@rud.is) |
|||
} |
@ -0,0 +1,21 @@ |
|||
% Generated by roxygen2: do not edit by hand |
|||
% Please edit documentation in R/read-dsstore.R |
|||
\name{read_dsstore} |
|||
\alias{read_dsstore} |
|||
\title{Read a \code{.DS_Store} file} |
|||
\usage{ |
|||
read_dsstore(path) |
|||
} |
|||
\arguments{ |
|||
\item{path}{a path to a valid \code{.DS_Store} file (\code{\link[=path.expand]{path.expand()}} will be called)} |
|||
} |
|||
\value{ |
|||
a character vector of filenames in the \code{.DS_Store} file or |
|||
a length 0 character vector if no parseable data was found |
|||
} |
|||
\description{ |
|||
Read a \code{.DS_Store} file |
|||
} |
|||
\examples{ |
|||
read_dsstore(system.file("extdat", "DS_Store.ctf", package = "mactheknife")) |
|||
} |
@ -0,0 +1,2 @@ |
|||
library(testthat) |
|||
test_check("mactheknife") |
@ -0,0 +1,6 @@ |
|||
context("minimal package functionality") |
|||
test_that("we can do something", { |
|||
|
|||
#expect_that(some_function(), is_a("data.frame")) |
|||
|
|||
}) |
Loading…
Reference in new issue