-
-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ID3v2: Fix about number pair of track and disk #149
ID3v2: Fix about number pair of track and disk #149
Conversation
2221d2c
to
e7e5f36
Compare
14b1404
to
f29200d
Compare
src/id3/v2/tag.rs
Outdated
@@ -25,6 +26,8 @@ const COMMENT_FRAME_ID: &str = "COMM"; | |||
const V4_MULTI_VALUE_SEPARATOR: char = '\0'; | |||
const NUMBER_PAIR_SEPARATOR: char = '/'; | |||
|
|||
const DEFAULT_NUMBER: u32 = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I suggest to use 0 as the default, which should never appear as a regular value.
DEFAULT_NUMBER_IN_PAIR
for disambiguation of the very generic name- Please add comments to both constants that these apply to both track and disk numbers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this part.
src/id3/v2/tag.rs
Outdated
@@ -285,6 +288,30 @@ impl ID3v2Tag { | |||
|
|||
(None, None) | |||
} | |||
|
|||
fn insert_frame(&mut self, item: TagItem) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose of this function is to insert an item by mapping it to the underlying representation, but it does not (always) insert a frame -> insert_item()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this part.
src/id3/v2/tag.rs
Outdated
fn set_number<F: FnMut(u32)>(item: TagItem, mut setter: F) { | ||
let number = item.into_value().text().map(str::parse::<u32>); | ||
|
||
if let Some(Ok(number)) = number { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Log a warning instead of silently ignoring values that couldn't be parsed successfully?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this part.
src/id3/v2/tag.rs
Outdated
@@ -349,6 +376,14 @@ fn new_picture_frame(picture: Picture, flags: FrameFlags) -> Frame<'static> { | |||
} | |||
} | |||
|
|||
fn merge_num_pair<N, T>(number: N, total: T) -> String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
format_number_pair()
? No need for abbreviations here.- Using optional arguments would eliminate redundant code in setters and make this function more versatile and testable in isolation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this part.
src/id3/v2/tag.rs
Outdated
} | ||
|
||
if let Some(total) = total { | ||
let number = value.unwrap_or_else(|| Cow::Owned(DEFAULT_NUMBER.to_string())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A const &str
representation for DEFAULT_NUMBER
would avoid an unecessary allocation -> value.as_deref().unwrap_or(DEFAULT_NUMBER_STR)
.
With the proposed changes and extension of merge_num_pair
this might even become obsolete.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not fixed it. Because merge_num_pair
was changed.
src/id3/v2/tag.rs
Outdated
}) | ||
} | ||
|
||
let number_pair_keys = &[ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be extracted as a constant of the module. Probably near the other number constants. Serves as additional documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed this part.
@@ -1634,4 +1723,103 @@ mod tests { | |||
|
|||
assert_eq!(id3v2, reparsed); | |||
} | |||
|
|||
#[test] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests only cover a single case. Missing are test for only number and only total.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am also curious to know what happens if the frame value
- is empty (contains only whitespace)
- "/"
- "/1"
- "1/"
This should be handled by lightweight unit tests, no need for heavy-weight file tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added tests about the missing tests at first.
I will add tests about the frame values.
@uklotzde Thank you for review. I will fix this PR. |
Take my proposals with a grain of salt. All my remarks in PRs are intended as additional input from an outside viewer who has a different perspective on the topic. They should support you to validate and rethink your design decisions or to reveal blank spaces and edge cases. A seemingly dumb question is often the most helpful feedback you could get for improving the code. |
f29200d
to
438f032
Compare
I fixed my existing code at first. I will add tests about the frame values. @uklotzde Thank you for comment. You have helped me to improve my PR more. |
438f032
to
b623dad
Compare
I fixed a log message only. I will add tests about the frame values. |
src/id3/v2/tag.rs
Outdated
fn insert_number_pair(&mut self, id: &'static str, number: Option<u32>, total: Option<u32>) { | ||
let content = format_number_pair(number, total); | ||
|
||
match content { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using if let Some() =
would be more idiomatic. I remember that clippy warns when matching on Option
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for comment. I fixed the code.
b623dad
to
8dc3166
Compare
I added tests about the frame values and fixed the code to except invalid values. |
src/id3/v2/tag.rs
Outdated
assert_invalid("/1"); | ||
assert_invalid("1/"); | ||
assert_invalid("a/b"); | ||
assert_invalid("1 / 2"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect that leading/trailing whitespaces around the numbers are trimmed.
src/id3/v2/tag.rs
Outdated
assert_invalid("1 / 2"); | ||
assert_invalid("1/2/3"); | ||
assert_invalid("1//2"); | ||
assert_invalid(" 1/2 "); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This input should also be considered as valid.
src/id3/v2/tag.rs
Outdated
|
||
let is_valid = if let Some(total) = total { | ||
content.len() == number.len() + 1 + total.len() | ||
&& str::parse::<u32>(number).is_ok() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By extracting a parse_number()
function you could handle the whitespace trimming consistently. I consider "total" as a special kind of number, so the naming should be reasonable and we do not need to name it parse_number_or_total()
.
@uklotzde Thank you for comments. I will modify my code. Before I do that, please allow to confirm the details about valid or invalid frames. I understood the following table, is it right?
|
I suggest to only ignore whitespace, because the overarching objective is to preserve all (meaningful) information. Consequently both I am unsure about |
@uklotzde Thank you for the comment. I will modify my code to ignore whitespaces. For the present, In other words, I plan to modify my code for the following table.
|
8dc3166
to
f85ce9f
Compare
I modified my code to ignore whitespaces while parsing a number pair. |
src/id3/v2/tag.rs
Outdated
total_key: ItemKey, | ||
) -> Option<()> { | ||
fn parse_number(source: Option<&str>) -> Option<&str> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should accept source: &str
. Otherwise you are implicitly re-implementing Option::and_then
.
src/id3/v2/tag.rs
Outdated
let number_source = split.next()?; | ||
let number = parse_number(Some(number_source))?; | ||
let total_source = split.next(); | ||
let total = parse_number(total_source); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let total = total_source.and_then(parse_number)
if str::parse::<u32>(number).is_ok() { | ||
Some(number) | ||
} else { | ||
log::warn!("{number:?} could not be parsed as a number."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging a warning if the input is empty is not desired.
f85ce9f
to
a437026
Compare
@uklotzde Thank you for the advice. I fixed my code. |
@@ -547,21 +607,54 @@ impl SplitAndMergeTag for ID3v2Tag { | |||
fn split_pair( | |||
content: &str, | |||
tag: &mut Tag, | |||
current_key: ItemKey, | |||
number_key: ItemKey, | |||
total_key: ItemKey, | |||
) -> Option<()> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Option<()>
for early returns is sometimes handy but also confusing. Better return nothing and use if-let-else statements for early returns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please allow me to confirm the details.
split_pair()
is called in the match
statement as the match guard. if let
guards are experimental now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I wanted to referr to let-else statements introduced with Rust 1.65. These should be convenient here and allow to avoid the final return of a meaningless result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for details. I will consider to use let-else.
src/id3/v2/tag.rs
Outdated
let total_source = split.next(); | ||
let total = total_source.and_then(parse_number); | ||
|
||
let is_valid = if let Some(total_source) = total_source { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check doesn't work. You will notice if you add tests for leading/trailing whitespace.
Implicitly assuming that the length of NUMBER_PAIR_SEPARATOR equals 1 byte is brittle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please allow me to confirm the details.
You will notice if you add tests for leading/trailing whitespace.
What test cases do you think? The followings are passed as a valid content when I added the unit test:
- " 1 "
- " 1 " (The spaces have 2 characters)
- "1 / 2" (The spaces have 2 characters)
- " 1 / 2 " (The spaces have 2 characters)
Because content
is checked the following:
- A not trimmed string has a number only.
- One separator character and two not trimmed strings that have a number only.
Implicitly assuming that the length of NUMBER_PAIR_SEPARATOR equals 1 byte is brittle.
May do not NUMBER_PAIR_SEPARATOR equals 1 bytes? I would be glad if you could provide an example.
Otherwise, do you mean that the constant should be extracted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number number 1 appears out of the context of NUMBER_PAIR_SEPARATOR
. If the constant was of type str
you could use NUMBER_PAIR_SEPARATOR.len()
. But using the size of the separator is probably no longer needed after fixing the parsing. It should not be relevant for the decision, only number and total with their parse results need to be considered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning an Option
from parse_number() is probably no sufficient, because you need to distinguish between empty and not a number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the low-level input representation again for making the final decision is conceptually unsound, different levels of abstraction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, I overlooked some tests. But I am still unsure that this code covers all cases correctly, especially the expression content.len() == number_source.len() + 1 + total_source.len()
. I already mentioned the various reasons why it is not suitable here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for details. I understand that I should improve parsing about content
. I will consider it.
src/id3/v2/tag.rs
Outdated
.push(TagItem::new(total_key, ItemValue::Text(total.to_string()))) | ||
} | ||
let number_source = split.next()?; | ||
let number = parse_number(number_source)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let Some(number) = parse_number(number_source) else {
return;
};
@uklotzde Thank you for comments. I replied for them. |
let number = source.trim(); | ||
|
||
if number.is_empty() { | ||
return None; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning an empty str
(= Some(number)
) would be an option to distinguish between empty and invalid numbers. There are many possibilities for encoding the 3 variants empty, number, and invalid.
While trying to get rid of the input string comparisons I came up with a simple and effective solution: mikanbako#1 |
@uklotzde Thank you for PR. It is merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this nice cooperation! The code is very solid now after we have considered many possible use cases. The tests will ensure that everything works as expected and are a good starting point if the behavior ever needs to be modified.
@Serial-ATA Please take a final look and then merge. The recent clippy failures seem to be unrelated and should be fixed separately, not by this PR.
Thanks for fixing this! And thanks @uklotzde for the review. This looks good, and has a lot of tests to back it up 😄. This makes me realize that the same issue will occur in APE tags as well, which use the same format for track/disk information. I'll have to make an issue for that. |
This PR fixes #145.
TODOs