14
submitted 22 hours ago* (last edited 12 hours ago) by such0299@sh.itjust.works to c/rust@programming.dev

Hello

I've been interested with Rust for the last year, but cannot find time to learn Rust.

Coming from Python, i have found my self quite difficult to learn Rust and grasp its concepts.

So, for the last couple of weeks, i have been able to spare some times to learn Rust, and created a mini project to parse Vendor OUI with Rust.

If you would be so kind to spare some of your time to review my code and give me some constructive feedback on how to tackle this, and some guidance for me about what i can improve because its not the rust way of doing things, i would be very happy. I want to improve my skills on Rust so i have another tools in my toolbox

This is not a promotion, because i believe there's another tools just like mine out there nor i want you to use my project, because this project is only for me to test my skill for using Rust.

Thank you in advanced :D

=== Additional text after posting ===

Thank you for those who reply, i really learned something new. I'll try to improve my code :D

Shout Out to Nous, Hades, Kwdg, BB_C

<3

top 12 comments
sorted by: hot top controversial new old
[-] nous@programming.dev 7 points 19 hours ago* (last edited 19 hours ago)

parse_oui_database takes in a file path as a &String that is used to open the file in a parsing function. IMO there are a number of problems here.

First, you should almost never take in a &String as a function argument. This basically means you have a reference to a owned object. It forces an allocation of everything passed into the function only to take a reference of it. It excludes types that are simply &strs forcing the caller to convert them to a full String - which involves an allocation. The function should just taking in a &str as it is cheap to convert a String to a &str (cheaper to use than a &String as well as &String have a double redirection).

Sometimes it might be even better might be to take in a impl<AsRef(str)> which means the function can take in anything that can be converted into a &str without the caller needing to do it directly. Though on larger functions like this that might not always be the best idea as it makes it generic and so will be monomorphised for every type you pass into it. This can bloat a binary if you do it on lots of large functions with lots of different input types. You can also get the best of both worlds with a generic wrapper function to a concrete implementation - so the large function has a concrete type &str and a warpper that takes a impl <AsRef<str>> and calls the inner function. Though in this case it is probably easier to just take in a &str and manually convert at all the one call sites.

Second. String/&str are not the write types for paths. Those would be PathBuf and &Path which both work like String and &str (so all the above applies to these as well). These are generally better to use as paths in most OSs dont have to be unicode. Which means there are files (though very rarely) which cannot be represented as a String. This is why File::open takes in a AsRef<Path> which your function can also.

Lastly. I would not conflate opening a file with parsing it. These should be two different functions. This makes the code a bit more flexible - you can get the file to parse from other sources. One big advantage to this would be for testing where you can just have the test data as strings in the test. It also makes the returned error type simpler as one function can deal with io errors and the other with parsing errors. And in this case you can just parse the file directly from the internet request rather than saving it to a file first (though there are other reasons you may or may not want to do this).

[-] such0299@sh.itjust.works 1 points 12 hours ago

First, you should almost never take in a &String as a function argument. This basically means you have a reference to a owned object. It forces an allocation of everything passed into the function only to take a reference of it. It excludes types that are simply &strs forcing the caller to convert them to a full String - which involves an allocation. The function should just taking in a &str as it is cheap to convert a String to a &str (cheaper to use than a &String as well as &String have a double redirection).

I think i need to explore more about pointer and reference? its still a new concept for me :D, so i just throw everything with &String without a strong understanding about the things it does in the background. Thank you for pointing that out.

Second. String/&str are not the write types for paths. Those would be PathBuf and &Path which both work like String and &str (so all the above applies to these as well). These are generally better to use as paths in most OSs dont have to be unicode. Which means there are files (though very rarely) which cannot be represented as a String. This is why File::open takes in a AsRef which your function can also.

What i learned from this is that a misconception of the parameter input. Instead of using String, i have to treat it as PathBuf?, I'll check it out later

Lastly. I would not conflate opening a file with parsing it. These should be two different functions. This makes the code a bit more flexible - you can get the file to parse from other sources. One big advantage to this would be for testing where you can just have the test data as strings in the test. It also makes the returned error type simpler as one function can deal with io errors and the other with parsing errors. And in this case you can just parse the file directly from the internet request rather than saving it to a file first (though there are other reasons you may or may not want to do this).

I do agree with this statement, I'll refactor my code to seperate the read and the parse logic

Thank you Nous.

PS: How do we tag other users? XD

[-] Rossphorus@lemmy.world 1 points 7 hours ago

Whenever you make a String you're saying that you need a string that can grow or shrink, and all the extra code required to make that work. Because it can grow or shrink it can't be stored on the (fast and efficient) stack, instead we need to ask the OS for some space on the heap, which is slow but usually has extra space around it so it can grow if needed. The OS gives back some heap space, which we can then use as a buffer to store the contents of the string.

If you just need to use the contents of a string you can accept a &str instead. A &str is really just a reference to an existing buffer (which can be either on the stack or in the heap), so if the buffer the user passes in is on the stack then we can avoid that whole 'asking the OS for heap space' part, and if it's on the heap then we can use the existing buffer on the heap at no extra cost. Compare this to taking a &String which is basically saying 'this string must be on the heap in case it grows or shrinks, but because it's an immutable reference I promise I won't grow or shrink it' which is a bit silly.

[-] hades@programming.dev 3 points 19 hours ago

Welcome to Rust, I hope you enjoyed learning and using it :)

One major advice would be to run cargo clippy, which gives a lot of helpful hints. Apart from that I also have some feedback.

    40	struct VendorOuiData {
    41	    vendor_organization: String, // Vendor Organization

These comments should be doc-comments (///) so that cargo doc picks them up.

    49	fn fetch_oui_database(url_string: &String) -> Result<String, Box<dyn std::error::Error>> {

Not saying that your return type is bad, but in general you might be interested in crates thiserror and anyhow that simplify error handling.

    51	        .timeout(Duration::new(60, 0))

Duration::from_mins(1) would be much more readable.

    66	                panic!("{}", err);

You should almost never panic. Your function already returns a Result<>, so you should be using that for error handling.

    70	    } else {
    71	        // HTTP Status is not 200

Not Rust specific, but usually I recommend to handle errors first, and return from the function early. This way you reduce cognitive load on the reader, and reduce nesting of if-blocks.

   150	                let mut parts = line.splitn(2, '\t');

There's a convenient method to split in two substrings that you could have used:

let (mac_address, vendor_organization) = line.split_once('\t').unwrap_or(("", ""));
   166	                vendor_oui_data.vendor_address += line
   167	                    .trim()
   168	                    .split(' ')
   169	                    .filter(|s| !s.is_empty())
   170	                    .collect::<Vec<_>>()
   171	                    .join(" ")
   172	                    .as_str();

I would probably write a regular expression that replaces all contiguous whitespace with a single space instead.

   173	                vendor_oui_data.vendor_address.push('\n');

Aren't you trimming this new line off in line 181?

   181	        vendor_oui_data.vendor_address = vendor_oui_data.vendor_address.trim().to_string();

This is somewhat inefficient, since you'll be allocating and copying your string here.

[-] such0299@sh.itjust.works 1 points 12 hours ago

Hello and Thank You :D Rust really intrigues me so i really want to explore more about Rust

cargo clippy I'll look for Clippy

These comments should be doc-comments (///) so that cargo doc picks them up. I see, i guess i skimmed the Rust Docs :D

but in general you might be interested in crates thiserror and anyhow that simplify error handling. Still, Rust Error Handling and Rust in General is still a big black box for me yet to be unveiled :D, why do i need crates for error handling? But I'll look into it. Still can't quite grasp the concept of Rust

Duration::from_mins(1) would be much more readable. Nice info, never knew this existed before, because i found it in some tutorial that timeout need something in Duration.

You should almost never panic. Your function already returns a Result<>, so you should be using that for error handling. Still figuring how to handle error, but just like what the others say, panic should never occurred, and always return the error

Not Rust specific, but usually I recommend to handle errors first, and return from the function early. This way you reduce cognitive load on the reader, and reduce nesting of if-blocks. That's actually correct, thank you for your input, i always ended with nasty nested if before because of this. Never thought about it before XD

Thank you for your input. I'll refactor my code

[-] nous@programming.dev 3 points 20 hours ago

You panic a lot on errors in functions where you return a result. Almost all of these are due to input problems not programming logic errors. These really should be return from the functions as errors so higher up functions can handle them how they might need to rather than just imminently crashing the program. Really panics should only be used when the situation should never occur and if it does that indicates a bug in the program - or you are being lazy with small/test code/one off scripts (but in this case why return any errors at all, you might as well just .unwrap() on everything instead of ?.

[-] such0299@sh.itjust.works 1 points 12 hours ago

To Panic or not to Panic Thank you for your pointer. I just remember this from the docs. So basically, the general rule is only to use panic when something bad really happened, and my program cannot continue right? otherwise always return the error?

Really panics should only be used when the situation should never occur and if it does that indicates a bug in the program

I'll rethink the use of panic in my code

would this section suffice? https://doc.rust-lang.org/book/ch09-00-error-handling.html

[-] Kwdg@discuss.tchncs.de 3 points 20 hours ago

I just scrolled through it, so no deep insights, but at first glance it looks pretty good.

One thing I saw, and that probably every rust programmer has to learn in the beginning, is to use slices for function parameters whenever prossible. Pretty much every time you use fn foo(s: &String) you should use fn foo(s: &str) and the same goes for &Vec<T> -> &[T]. Calling these works still the same so just changing the function definitions should work.

The advantage of this is, that now you can use every type that can be dereferenced as the slice, so you can now call foo like this foo("bar"); or with a String foo(&String::from("bar"));. The same applies for &[T] which can be called with &Vec<T> or an array &[T; N]

[-] BB_C@programming.dev 2 points 20 hours ago* (last edited 20 hours ago)

Only skimmed quickly.

  • Four different hex representations for the same 3 bytes is extremely wasteful. You don't need to store more than a [u8; 3]. You can create a "mapped" struct with those representations if caching them is somehow useful for the use-case. And since we're micro-optimizing memory usage, Box<str> instead of String (for the two fields with actual string data) would be more efficient since those values are not meant to be mutable. You can then serialize to one of the many efficient binary formats around (e.g. borsh). This is not JavaScript, so you can oftentimes not be bound to JSON.
  • Your parsing code looks both unrobust (potentially fragile hard-coded assumptions) and unnecessarily complex. Something much simpler can probably be done with some trivial regex usage.
[-] such0299@sh.itjust.works 1 points 14 hours ago

Box instead of String

I need to explore more about this. I really have no idea about this

Your parsing code looks both unrobust (potentially fragile hard-coded assumptions) and unnecessarily complex. Something much simpler can probably be done with some trivial regex usage.

My first approach is using regex, but it seems like overkill? because the data from https://standards-oui.ieee.org/oui/oui.txt is somehow already structured.

But i'll try the regex approach later.

Thank you :D

[-] nous@programming.dev 1 points 20 hours ago

This is a pet peeve of mine, but don't put main last. Its like opening a book to be greeted by a random chapter - probably one near the end and you have to hunt through it to find where the story actually starts, which is probably near the end.

This is a IMO horribly hangup from languages that require you to declare something before you can use it. You don't need to do that in rust. So put your functions in order that makes sense to read them from top to bottom. This typically means main should be one of the first functions you see - as it is the entry point to the code. In other files this might be the main functions a user is expected to use first. Sometimes you might want to see some datastructures before that. But overall things should be ordered by how it makes sense to read them to make it easier to make sense of the program.

[-] such0299@sh.itjust.works 1 points 14 hours ago

This is a IMO horribly hangup from languages that require you to declare something before you can use it. You don’t need to do that in rust. So put your functions in order that makes sense to read them from top to bottom.

Hello, nous.

Thank you for your feedback. I just know today that in rust, you can do this. I do agree with you about how to order things so it can convey more meaning to the reader. I'll keep that in mind in my next iterations.

Thank you

this post was submitted on 19 Mar 2026
14 points (93.8% liked)

Rust

7864 readers
39 users here now

Welcome to the Rust community! This is a place to discuss about the Rust programming language.

Wormhole

!performance@programming.dev

Credits

  • The icon is a modified version of the official rust logo (changing the colors to a gradient and black background)

founded 2 years ago
MODERATORS