Swift set some string value to file's attribute - swift

According to Apple's document, we can set lots of attributes for a file
A dictionary containing as keys the attributes to set for path and as values the corresponding value for the attribute. You can set the following attributes: busy, creationDate, extensionHidden, groupOwnerAccountID, groupOwnerAccountName, hfsCreatorCode, hfsTypeCode, immutable, modificationDate, ownerAccountID, ownerAccountName, posixPermissions. You can change single attributes or any combination of attributes; you need not specify keys for all attributes.
I want to set an extra parameter for a file. The parameter is a String, I can't find any attribute that I can set String.
For example, I tried
try FileManager.default.setAttributes([FileAttributeKey.ownerAccountName: NSString(string: "0B2TwsHM7lBpSMU1tNXVfSEp0RGs"), FileAttributeKey.creationDate: date], ofItemAtPath: filePath.path)
But when I load keys
let keys = try FileManager.default.attributesOfItem(atPath: filePath.path)
print(keys)
I only get .creationDate changed
[__C.FileAttributeKey(_rawValue: NSFileType): NSFileTypeRegular,
__C.FileAttributeKey(_rawValue: NSFilePosixPermissions): 420,
__C.FileAttributeKey(_rawValue: NSFileSystemNumber): 16777220,
__C.FileAttributeKey(_rawValue: NSFileReferenceCount): 1,
__C.FileAttributeKey(_rawValue: NSFileGroupOwnerAccountName): staff,
__C.FileAttributeKey(_rawValue: NSFileSystemFileNumber): 8423614,
__C.FileAttributeKey(_rawValue: NSFileGroupOwnerAccountID): 20,
__C.FileAttributeKey(_rawValue: NSFileModificationDate): 2017-08-16 06:03:57 +0000,
__C.FileAttributeKey(_rawValue: NSFileCreationDate): 1970-01-01 00:33:20 +0000,
__C.FileAttributeKey(_rawValue: NSFileSize): 9795,
__C.FileAttributeKey(_rawValue: NSFileExtensionHidden): 0,
__C.FileAttributeKey(_rawValue: NSFileOwnerAccountID): 501]
Is there any way that I can set string value to FileAttribute?

The documentation says "You can set the following attributes". That means you can only set those specific attributes via those API's.
What you are really looking for are Extended Attributes, and these use a separate set of (C-style) API's.
Something like:
let directory = NSTemporaryDirectory()
let someExampleText = "some sample text goes here"
do {
try someExampleText.write(toFile: "\(directory)/test.txt", atomically: true, encoding: .utf8)
} catch let error {
print("error while writing is \(error.localizedDescription)")
}
let valueString = "setting some value here"
let result = setxattr("\(directory)/test.txt", "com.stackoverflow.test", valueString, valueString.characters.count, 0, 0)
print("result is \(result) ; errno is \(errno)")
if errno != 0
{
perror("could not save")
}
let sizeOfRetrievedValue = getxattr("\(directory)/test.txt", "com.stackoverflow.test", nil, 0, 0, 0)
var data = Data(count: sizeOfRetrievedValue)
let newResult = data.withUnsafeMutableBytes({
getxattr("\(directory)/test.txt", "com.stackoverflow.test", $0, data.count, 0, 0)
})
if let resultString = String(data: data, encoding: .utf8)
{
print("retrieved string is \(resultString)")
}

Related

How to read a null terminated String from Data?

An iOS/Swift library delivers a Data object containing a null terminated string.
When converting it to a String by calling String(data: dataInstance, encoding: .utf8), the returned String ends with "\0".
Question now is, how do you convert the Data instance to a String without having "\0" appended? I.e. how can you omit the null terminating character at the end?
Trying to just .trimmingCharacters(in: .whitespacesAndNewlines) doesn't have any effect, so your advise is very much appreciated. Thank you.
A possible solution: Determine the index of the terminating zero, and convert only the preceding part of the data:
let data = Data([65, 66, 0, 67, 0])
let end = data.firstIndex(where: { $0 == 0 }) ?? data.endIndex
if let string = String(data: data[..<end], encoding:.utf8) {
print(string.debugDescription) // "AB"
}
If the data does not contain a null byte then everything will be converted.
You can do some pointer operations and use the init(cString:) initialiser, which takes a null terminated string.
let resultString = data.withUnsafeBytes { buffer in
guard let pointer = buffer.baseAddress?.assumingMemoryBound(to: CChar.self) else {
return ""
}
return String(cString: pointer)
}

Using Swift .split to format an array

I am reading in a text file of translation pairs of this format:
boy:garçon
garçon:boy
Into an array using the following code:
var vocab:[String:String] = [:]
let path = Bundle.main.path(forResource: "words_alpha", ofType: "txt")!
let text = try! String(contentsOfFile: path, encoding: String.Encoding.utf8)
let vocab = text.components(separatedBy: CharacterSet.newlines)
The imported array looks like this:
["boy:garçon", "garçon:boy"]
Whereas I would like the array to be formatted like this:
["boy":"garçon", "garçon":"boy"]
What is the best way to achieve the desired array format shown above using a Swift string transformation?
Have been trying to use .split, but with not much success.
Let's be clear:
["boy":"garçon", "garçon":"boy"]
That's a Dictionary, not an Array.
There a multiples ways to do that, here's two possible codes:
var manual: [String: String] = [:]
array.forEach { aString in
let components = aString.components(separatedBy: ":")
guard components.count == 2 else { return }
manual[components[0]] = components[1]
}
print(manual)
or
let reduced = array.reduce(into: [String: String]()) { result, current in
let components = current.components(separatedBy: ":")
guard components.count == 2 else { return }
result[components[0]] = components[1]
}
print(reduced)
Output (for both):
$> ["garçon": "boy", "boy": "garçon"]
As said, it's a Dictionary, so there is no guarantee that the print be:
["garçon": "boy", "boy": "garçon"] or ["boy":"garçon", "garçon":"boy"], it's key-value access, not index-value access.

Swift, how to get an image "where from" metadata field?

I'm reading a file from the file system on macOS, I tried to extract the metadata with the following code:
let imagedata = try? Data(contentsOf: fileUrl)
if imagedata == nil {
print("Could not read contents of image")
return
}
var source: CGImageSource = CGImageSourceCreateWithData((imagedata as! CFMutableData), nil)!
var metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [AnyHashable: Any]
print("image metadata", metadata)
And this is the output that I get:
image metadata Optional([AnyHashable("ColorModel"): RGB, AnyHashable("DPIHeight"): 72, AnyHashable("PixelHeight"): 840, AnyHashable("ProfileName"): sRGB IEC61966-2.1, AnyHashable("PixelWidth"): 840, AnyHashable("{PNG}"): {
Chromaticities = (
"0.3127",
"0.329",
"0.64",
"0.33",
"0.3",
"0.6000000000000001",
"0.15",
"0.06"
);
Gamma = "0.45455";
InterlaceType = 0;
XPixelsPerMeter = 2834;
YPixelsPerMeter = 2834;
sRGBIntent = 0;
}, AnyHashable("DPIWidth"): 72, AnyHashable("Depth"): 8])
And that extracts the usual metadata, but I actually would like to know where the image was downloaded from, when I drag n' drop an image into the desktop and then right click and then get info, I see the field:
Is there any way to retrieve this field?
Edit: I tried this:
if let attr = try? FileManager.default.attributesOfItem(atPath: fileUrl.path) {
print("attributes", attr)
}
and now I can see the kMDItemWhereFroms field, but I don't know how to reach it or parse. Maybe someone has an idea here?
I think it's always nice to show how tinker, how to find the data, so I'll show how I got it, while I didn't know at all about it.
First thought, it might not be in image Metadata, but in "File System" ones, so let's look at FileManager. It's in FileManager.attributesOfItem(atPath:)
Step 1:
let attributes = try fileManager.attributesOfItem(atPath: filePath)
This gives me:
[__C.NSFileAttributeKey(_rawValue: NSFileGroupOwnerAccountName): staff,
__C.NSFileAttributeKey(_rawValue: NSFileReferenceCount): 1,
__C.NSFileAttributeKey(_rawValue: NSFileModificationDate): 2020-05-13 15:16:20 +0000,
__C.NSFileAttributeKey(_rawValue: NSFileSystemFileNumber): 8635383780,
__C.NSFileAttributeKey(_rawValue: NSFilePosixPermissions): 420,
__C.NSFileAttributeKey(_rawValue: NSFileType): NSFileTypeRegular,
__C.NSFileAttributeKey(_rawValue: NSFileOwnerAccountID): 501,
__C.NSFileAttributeKey(_rawValue: NSFileGroupOwnerAccountID): 20,
__C.NSFileAttributeKey(_rawValue: NSFileExtensionHidden): 0,
__C.NSFileAttributeKey(_rawValue: NSFileHFSTypeCode): 0,
__C.NSFileAttributeKey(_rawValue: NSFileCreationDate): 2020-05-13 15:16:20 +0000,
__C.NSFileAttributeKey(_rawValue: NSFileOwnerAccountName): Larme,
__C.NSFileAttributeKey(_rawValue: NSFileSize): 1258299,
__C.NSFileAttributeKey(_rawValue: NSFileSystemNumber): 16777220,
__C.NSFileAttributeKey(_rawValue: NSFileExtendedAttributes): {
"com.apple.lastuseddate#PS" = {length = 16, bytes = 0x440fbc5e00000000503f091700000000};
"com.apple.macl" = {length = 72, bytes = 0x02009685 15175d5d 47089537 71b6a786 ... 00000000 00000000 };
"com.apple.metadata:kMDItemDownloadedDate" = {length = 53, bytes = 0x62706c69 73743030 a1013341 c2362362 ... 00000000 00000013 };
"com.apple.metadata:kMDItemWhereFroms" = {length = 414, bytes = 0x62706c69 73743030 a201025f 11013b68 ... 00000000 00000178 };
"com.apple.quarantine" = {length = 57, bytes = 0x30303833 3b356562 63306634 343b5361 ... 44344445 45343845 };
}, __C.NSFileAttributeKey(_rawValue: NSFileHFSCreatorCode): 0]
What to look in this info:
[...
__C.NSFileAttributeKey(_rawValue: NSFileExtendedAttributes): {
...
"com.apple.metadata:kMDItemWhereFroms" = {length = 414, bytes = 0x62706c69 73743030 a201025f 11013b68 ... 00000000 00000178 };
...
]
Ok, so let's continue to step 2, let's dig deeper:
let extendedAttributes = attributes[FileAttributeKey(rawValue: "NSFileExtendedAttributes")] as! [String: Any]
let data = extendedAttributes["com.apple.metadata:kMDItemWhereFroms"] as! Data //that's the new output for `Data` in new systems (iOS13+, etc.)
As always, I tend to do just in case to get more info: let str = String(data: data, encoding: .utf8) which returns nil. Let's remember that not all data is convertible into utf8 string like that. For instance, an image won't make it.
But I know a few tricks, let's do instead: let str = String(data: data, encoding: .ascii), this gives me:
"bplist00¢\u{01}\u{02}_\u{11}\u{01};https://doc-04-6k-docs.googleusercontent.com/docs/securesc/teo5l5s1g996hre86q7qv786r7me56c3/3nneqai4sh4lvk3bdlnpokt8793t4t5t/1589382975000/11847256646728493158/11847256646728493158/0B1U9OypmOvxpODVFb2VGZEdpckk?e=download&authuser=0&nonce=faav7fnl402re&user=11847256646728493158&hash=vl51p8m313rnnao3dleqsl348rp2vo82_\u{10}+https://drive.google.com/drive/u/0/my-drive\0\u{08}\0\u{0B}\u{01}J\0\0\0\0\0\0\u{02}\u{01}\0\0\0\0\0\0\0\u{03}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{01}x"
I downloaded my sample from my Google Drive, info seems making sense. What do see there?: bplist, that's the most important one here.
A PropertyList? Let's decode it then:
let urls = try PropertyListDecoder().decode([String].self, from: data)
It made sense for me to be a [String], it would have been different, that would had be harder. But it's a simple one.
Final code:
static func downloadedFromURLs(for filePath: String) -> [String]? {
let fileManager = FileManager.default
guard fileManager.fileExists(atPath: filePath) else { print("File doesn't exists"); return nil }
do {
let attributes = try fileManager.attributesOfItem(atPath: filePath)
guard let extendedAttributes = attributes[FileAttributeKey(rawValue: "NSFileExtendedAttributes")] as? [String: Any] else {
print("Didn't find NSFileExtendedAttributes value")
return nil
}
guard let data = extendedAttributes["com.apple.metadata:kMDItemWhereFroms"] as? Data else {
print("Didn't find com.apple.metadata:kMDItemWhereFroms value")
return nil
}
let urls = try PropertyListDecoder().decode([String].self, from: data)
return urls
} catch {
print("Error: \(error)")
return nil
}
}
Side note:
There might be some constant instead of writing hard coded strings for "com.apple.metadata:kMDItemWhereFroms"
and NSFileExtendedAttributes, but I didn't see it here.
Some related interested discussion.
https://apple.stackexchange.com/questions/110239/where-is-the-where-from-meta-data-stored-when-downloaded-via-chrome

Using Swift to write a character to a file

I'm trying to write a Swift program that writes a single character to a file. I've researched this but so far haven't figured out how to do this (note, I'm new to Swift). Note that the text file I'm reading and writing to can contain a series of characters, one per line. I want to read the last character and update the file so it only contains that last character.
Here's what I have so far:
let will_file = "/Users/willf/Drobox/foo.txt"
do {
let statusStr = try String(contentsOfFile: will_file, encoding: .utf8)
// find the last character in the string
var strIndex = statusStr.index(statusStr.endIndex, offsetBy: -1)
if statusStr[strIndex] == "\n" {
// I need to access the character just before the last \n
strIndex = statusStr.index(statusStr.endIndex, offsetBy: -2)
}
if statusStr[strIndex] == "y" {
print("yes")
} else if statusStr[strIndex] == "n" {
print("no")
} else {
// XXX deal with error here
print("The char isn't y or n")
}
// writing
// I get a "cannot invoke 'write with an arg list of type (to: String)
try statusStr[strIndex].write(to: will_file)
}
I would appreciate advice on how to write the character returned by statusStr[strIndex].
I will further point out that I have read this Read and write a String from text file but I am still confused as to how to write to a text file under my Dropbox folder. I was hoping that there was a write method that could take an absolute path as a string argument but I have not found any doc or code sample showing how to do this that will compile in Xcode 9.2. I have also tried the following code which will not compile:
let dir = FileManager.default.urls(for: .userDirectory, in: .userDomainMask).first
let fileURL = dir?.appendingPathComponent("willf/Dropbox/foo.txt")
// The compiler complains about extra argument 'atomically' in call
try statusStr[strIndex].write(to: fileURL, atomically: false, encoding: .utf8)
I have figured out how to write a character as a string to a file thanks to a couple answers on stack overflow. The key is to coerce a character type to a string type because the string object supports the write method I want to use. Note that I used both the answers in Read and write a String from text file and in Swift Converting Character to String to come up with the solution. Here is the Swift code:
import Cocoa
let will_file = "/Users/willf/Dropbox/foo.txt"
do {
// Read data from will_file into String object
let statusStr = try String(contentsOfFile: will_file, encoding: .utf8)
// find the last character in the string
var strIndex = statusStr.index(statusStr.endIndex, offsetBy: -1)
if statusStr[strIndex] == "\n" {
// I need to access the character just before the last \n
strIndex = statusStr.index(statusStr.endIndex, offsetBy: -2)
}
if statusStr[strIndex] != "n" && statusStr[strIndex] != "y" {
// XXX deal with error here
print("The char isn't y or n")
}
// Update file so it contains only the last status char
do {
// String(statusStr[strIndex]) coerces the statusStr[strIndex] character to a string for writing
try String(statusStr[strIndex]).write(toFile: will_file, atomically: false, encoding: .utf8)
} catch {
print("There was a write error")
}
} catch {
print("there is an error!")
}

What does .decode return in swift 4?

When you call .decode() to decode a struct, what exactly does it return?
I have look it up on the Apple Documentation, but all it says is "native format into in-memory representations." But what does this mean? Can anyone help me?
I'm asking this because my app is crashing when I get a null value from the JSON data, from this line of code:
let plantData = try decoder.decode([Plants].self, from: data)
Here is my struct:
struct Plants: Codable {
let date: String
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And Here is my Parsing code:
func parseJson() {
let url = URL(string: ebr_String)
// Load the URL
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
// If there are any errors don't try to parse it, show the error
guard let data = data, error == nil else { print(error!); return }
let decoder = JSONDecoder()
do{
let plantData = try decoder.decode([Plants].self, from: data)
print(plantData)
And Here is just a snippet of the information I am getting back:
MorrowTabbedApp.Plants(date: "2018-02-22", monthlyAVG: Optional("1210.06")), MorrowTabbedApp.Plants(date: "2018-02-23", monthlyAVG: nil)]
Here is the snippet of JSON from the web:
[
{"Date":"2018-02-21","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-22","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-23","30_Day_MA_MMBTU":null}
]
The decode method of JSONDecoder is a "generic" method. It returns an instance of whatever type you specified in the first parameter of the method. In your case, it returns a [Plants], i.e. a Array<Plants>, i.e. a Swift array of Plants instances.
If it's crashing because of a null value in your JSON, then you have to identify what was null, whether it was appropriate to be null, and if so, make sure that any Plants properties associated with values that might be null should be optionals.
Given your updated answer with code snippets, I'd suggest:
// Personally, I'd call this `Plant` as it appears to represent a single instance
struct Plant: Codable {
let date: String
let monthlyAVG: String? // Or you can use `String!` if you don't want to manually unwrap this every time you use it
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And:
do {
let plantData = try JSONDecoder().decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
Note the filter line which selects only those occurrences for which monthlyAVG is not nil.
A couple of other suggestions:
Personally, if you could, I'd rather see the web service designed to only return the values you want (those with an actual monthlyAVG) and then change the monthlyAVG property to not be an optional. But that's up to you.
If monthlyAVG is really a numeric average, I'd change the web service to not return it as a string at all, but as a number without quotes. And then change the property of Plant to be Double or whatever.
You could, if you wanted, change the date property to be a Date and then use dateDecodingStrategy to convert the string to a Date:
struct Plant: Codable {
let date: Date
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
and
do {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
let plantData = try decoder.decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
You might do this if, for example, you wanted the x-axis of your chart to actually represent time rather than an evenly spaced set of data points.